html-minifier-next 4.15.2 → 4.16.1
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 +15 -17
- package/dist/htmlminifier.cjs +186 -43
- package/dist/htmlminifier.esm.bundle.js +186 -43
- package/dist/types/lib/attributes.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/dist/types/tokenchain.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/lib/attributes.js +10 -2
- package/src/lib/svg.js +162 -18
- package/src/tokenchain.js +15 -23
package/README.md
CHANGED
|
@@ -358,41 +358,39 @@ How does HTML Minifier Next compare to other minifiers? (All minification with t
|
|
|
358
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
359
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
360
360
|
| [A List Apart](https://alistapart.com/) | 59 | **50** | 51 | 52 | 51 | 54 | 52 |
|
|
361
|
-
| [Apple](https://www.apple.com/) |
|
|
362
|
-
| [BBC](https://www.bbc.co.uk/) |
|
|
361
|
+
| [Apple](https://www.apple.com/) | 211 | **177** | 188 | 190 | 191 | 192 | 193 |
|
|
362
|
+
| [BBC](https://www.bbc.co.uk/) | 675 | **614** | 633 | 634 | 635 | 669 | n/a |
|
|
363
363
|
| [CERN](https://home.cern/) | 152 | **83** | 91 | 91 | 91 | 93 | 96 |
|
|
364
364
|
| [CSS-Tricks](https://css-tricks.com/) | 162 | **119** | 127 | 143 | 143 | 148 | 144 |
|
|
365
365
|
| [ECMAScript](https://tc39.es/ecma262/) | 7250 | **6401** | 6573 | 6455 | 6578 | 6626 | n/a |
|
|
366
366
|
| [EDRi](https://edri.org/) | 80 | **59** | 70 | 70 | 71 | 75 | 73 |
|
|
367
|
-
| [EFF](https://www.eff.org/) |
|
|
367
|
+
| [EFF](https://www.eff.org/) | 54 | **45** | 49 | 47 | 48 | 49 | 49 |
|
|
368
368
|
| [European Alternatives](https://european-alternatives.eu/) | 48 | **30** | 32 | 32 | 32 | 32 | 32 |
|
|
369
|
-
| [FAZ](https://www.faz.net/aktuell/) |
|
|
369
|
+
| [FAZ](https://www.faz.net/aktuell/) | 1579 | 1468 | **1414** | 1503 | 1514 | 1525 | n/a |
|
|
370
370
|
| [French Tech](https://lafrenchtech.gouv.fr/) | 153 | **122** | 126 | 126 | 126 | 132 | 127 |
|
|
371
|
-
| [Frontend Dogma](https://frontenddogma.com/) | 225 | **217** | 238 | 223 | 225 |
|
|
372
|
-
| [Google](https://www.google.com/) |
|
|
373
|
-
| [Ground News](https://ground.news/) | 2373 | **2089** | 2185 | 2211 | 2213 | 2360 | n/a |
|
|
371
|
+
| [Frontend Dogma](https://frontenddogma.com/) | 225 | **217** | 238 | 223 | 225 | 244 | 225 |
|
|
372
|
+
| [Google](https://www.google.com/) | 18 | **16** | 17 | 17 | 17 | 18 | 18 |
|
|
374
373
|
| [HTML Living Standard](https://html.spec.whatwg.org/multipage/) | 149 | 148 | 153 | **147** | 149 | 155 | 149 |
|
|
375
374
|
| [Igalia](https://www.igalia.com/) | 50 | **33** | 36 | 36 | 36 | 37 | 36 |
|
|
376
|
-
| [Leanpub](https://leanpub.com/) |
|
|
375
|
+
| [Leanpub](https://leanpub.com/) | 231 | **202** | 216 | 216 | 217 | 227 | 229 |
|
|
377
376
|
| [Mastodon](https://mastodon.social/explore) | 37 | **28** | 32 | 35 | 35 | 36 | 36 |
|
|
378
|
-
| [MDN](https://developer.mozilla.org/en-US/) |
|
|
379
|
-
| [Middle East Eye](https://www.middleeasteye.net/) | 223 | **
|
|
380
|
-
| [Mistral AI](https://mistral.ai/) |
|
|
377
|
+
| [MDN](https://developer.mozilla.org/en-US/) | 106 | **60** | 62 | 63 | 63 | 66 | 66 |
|
|
378
|
+
| [Middle East Eye](https://www.middleeasteye.net/) | 223 | **197** | 203 | 201 | 201 | 203 | 204 |
|
|
379
|
+
| [Mistral AI](https://mistral.ai/) | 361 | **319** | 324 | 326 | 327 | 357 | n/a |
|
|
381
380
|
| [Mozilla](https://www.mozilla.org/) | 45 | **31** | 34 | 34 | 34 | 35 | 35 |
|
|
382
381
|
| [Nielsen Norman Group](https://www.nngroup.com/) | 86 | 68 | **55** | 74 | 75 | 77 | 76 |
|
|
383
|
-
| [SitePoint](https://www.sitepoint.com/) | 491 | **360** | 431 | 465 | 470 | 488 | n/a |
|
|
384
382
|
| [Startup-Verband](https://startupverband.de/) | 42 | **29** | 30 | 30 | 30 | 31 | 30 |
|
|
385
|
-
| [TetraLogical](https://tetralogical.com/) | 44 |
|
|
383
|
+
| [TetraLogical](https://tetralogical.com/) | 44 | 38 | **35** | 38 | 39 | 39 | 39 |
|
|
386
384
|
| [TPGi](https://www.tpgi.com/) | 175 | **159** | 160 | 164 | 166 | 172 | 172 |
|
|
387
|
-
| [United Nations](https://www.un.org/en/) |
|
|
385
|
+
| [United Nations](https://www.un.org/en/) | 152 | **112** | 121 | 125 | 125 | 130 | 123 |
|
|
388
386
|
| [Vivaldi](https://vivaldi.com/) | 92 | **74** | n/a | 79 | 81 | 83 | 81 |
|
|
389
387
|
| [W3C](https://www.w3.org/) | 50 | **36** | 39 | 38 | 38 | 41 | 39 |
|
|
390
|
-
| **Average processing time** | |
|
|
388
|
+
| **Average processing time** | | 120 ms (28/28) | 138 ms (27/28) | 41 ms (28/28) | **14 ms (28/28)** | 283 ms (28/28) | 1349 ms (24/28) |
|
|
391
389
|
|
|
392
|
-
(Last updated: Dec
|
|
390
|
+
(Last updated: Dec 26, 2025)
|
|
393
391
|
<!-- End auto-generated -->
|
|
394
392
|
|
|
395
|
-
Notes:
|
|
393
|
+
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.
|
|
396
394
|
|
|
397
395
|
## Examples
|
|
398
396
|
|
package/dist/htmlminifier.cjs
CHANGED
|
@@ -624,37 +624,29 @@ class Sorter {
|
|
|
624
624
|
for (let i = 0, len = this.keys.length; i < len; i++) {
|
|
625
625
|
const token = this.keys[i];
|
|
626
626
|
|
|
627
|
-
//
|
|
628
|
-
|
|
627
|
+
// Single pass: Count matches and collect non-matches
|
|
628
|
+
let matchCount = 0;
|
|
629
|
+
const others = [];
|
|
630
|
+
|
|
629
631
|
for (let j = fromIndex; j < tokens.length; j++) {
|
|
630
632
|
if (tokens[j] === token) {
|
|
631
|
-
|
|
633
|
+
matchCount++;
|
|
634
|
+
} else {
|
|
635
|
+
others.push(tokens[j]);
|
|
632
636
|
}
|
|
633
637
|
}
|
|
634
638
|
|
|
635
|
-
if (
|
|
636
|
-
//
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
for (let j = 0; j < positions.length; j++) {
|
|
641
|
-
result.push(token);
|
|
639
|
+
if (matchCount > 0) {
|
|
640
|
+
// Rebuild: `matchCount` instances of token first, then others
|
|
641
|
+
let writeIdx = fromIndex;
|
|
642
|
+
for (let j = 0; j < matchCount; j++) {
|
|
643
|
+
tokens[writeIdx++] = token;
|
|
642
644
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
const posSet = new Set(positions);
|
|
646
|
-
for (let j = fromIndex; j < tokens.length; j++) {
|
|
647
|
-
if (!posSet.has(j)) {
|
|
648
|
-
result.push(tokens[j]);
|
|
649
|
-
}
|
|
645
|
+
for (let j = 0; j < others.length; j++) {
|
|
646
|
+
tokens[writeIdx++] = others[j];
|
|
650
647
|
}
|
|
651
648
|
|
|
652
|
-
|
|
653
|
-
for (let j = 0; j < result.length; j++) {
|
|
654
|
-
tokens[fromIndex + j] = result[j];
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
const newFromIndex = fromIndex + positions.length;
|
|
649
|
+
const newFromIndex = fromIndex + matchCount;
|
|
658
650
|
return this.sorterMap.get(token).sort(tokens, newFromIndex);
|
|
659
651
|
}
|
|
660
652
|
}
|
|
@@ -1260,8 +1252,31 @@ async function processScript(text, options, currentAttrs, minifyHTML) {
|
|
|
1260
1252
|
* - Numeric precision reduction for coordinates and path data
|
|
1261
1253
|
* - Whitespace removal in attribute values (numeric sequences)
|
|
1262
1254
|
* - Default attribute removal (safe, well-documented defaults)
|
|
1263
|
-
* - Color minification (hex shortening, rgb() to hex)
|
|
1255
|
+
* - Color minification (hex shortening, rgb() to hex, named colors)
|
|
1256
|
+
* - Identity transform removal
|
|
1257
|
+
* - Path data space optimization
|
|
1258
|
+
*/
|
|
1259
|
+
|
|
1260
|
+
|
|
1261
|
+
// Cache for minified numbers
|
|
1262
|
+
const numberCache = new LRU(100);
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* Named colors that are shorter than their hex equivalents
|
|
1266
|
+
* Only includes cases where using the name saves bytes
|
|
1264
1267
|
*/
|
|
1268
|
+
const NAMED_COLORS = {
|
|
1269
|
+
'#f00': 'red', // #f00 (4) → red (3), saves 1
|
|
1270
|
+
'#c0c0c0': 'silver', // #c0c0c0 (7) → silver (6), saves 1
|
|
1271
|
+
'#808080': 'gray', // #808080 (7) → gray (4), saves 3
|
|
1272
|
+
'#800000': 'maroon', // #800000 (7) → maroon (6), saves 1
|
|
1273
|
+
'#808000': 'olive', // #808000 (7) → olive (5), saves 2
|
|
1274
|
+
'#008000': 'green', // #008000 (7) → green (5), saves 2
|
|
1275
|
+
'#800080': 'purple', // #800080 (7) → purple (6), saves 1
|
|
1276
|
+
'#008080': 'teal', // #008080 (7) → teal (4), saves 3
|
|
1277
|
+
'#000080': 'navy', // #000080 (7) → navy (4), saves 3
|
|
1278
|
+
'#ffa500': 'orange' // #ffa500 (7) → orange (6), saves 1
|
|
1279
|
+
};
|
|
1265
1280
|
|
|
1266
1281
|
/**
|
|
1267
1282
|
* Default SVG attribute values that can be safely removed
|
|
@@ -1295,7 +1310,22 @@ const SVG_DEFAULT_ATTRS = {
|
|
|
1295
1310
|
opacity: value => value === '1',
|
|
1296
1311
|
visibility: value => value === 'visible',
|
|
1297
1312
|
display: value => value === 'inline',
|
|
1298
|
-
|
|
1313
|
+
// Note: Overflow handled especially in `isDefaultAttribute` (not safe for root `<svg>`)
|
|
1314
|
+
|
|
1315
|
+
// Clipping and masking defaults
|
|
1316
|
+
'clip-rule': value => value === 'nonzero',
|
|
1317
|
+
'clip-path': value => value === 'none',
|
|
1318
|
+
mask: value => value === 'none',
|
|
1319
|
+
|
|
1320
|
+
// Marker defaults
|
|
1321
|
+
'marker-start': value => value === 'none',
|
|
1322
|
+
'marker-mid': value => value === 'none',
|
|
1323
|
+
'marker-end': value => value === 'none',
|
|
1324
|
+
|
|
1325
|
+
// Filter and color defaults
|
|
1326
|
+
filter: value => value === 'none',
|
|
1327
|
+
'color-interpolation': value => value === 'sRGB',
|
|
1328
|
+
'color-interpolation-filters': value => value === 'linearRGB'
|
|
1299
1329
|
};
|
|
1300
1330
|
|
|
1301
1331
|
/**
|
|
@@ -1305,6 +1335,20 @@ const SVG_DEFAULT_ATTRS = {
|
|
|
1305
1335
|
* @returns {string} Minified numeric string
|
|
1306
1336
|
*/
|
|
1307
1337
|
function minifyNumber(num, precision = 3) {
|
|
1338
|
+
// Fast path for common values (avoids parsing and caching)
|
|
1339
|
+
if (num === '0' || num === '1') return num;
|
|
1340
|
+
// Common decimal variants that tools export
|
|
1341
|
+
if (num === '0.0' || num === '0.00' || num === '0.000') return '0';
|
|
1342
|
+
if (num === '1.0' || num === '1.00' || num === '1.000') return '1';
|
|
1343
|
+
|
|
1344
|
+
// Check cache
|
|
1345
|
+
// (Note: uses input string as key, so “0.0000” and “0.00000” create separate entries.
|
|
1346
|
+
// This is intentional to avoid parsing overhead.
|
|
1347
|
+
// Real-world SVG files from export tools typically use consistent formats.)
|
|
1348
|
+
const cacheKey = `${num}:${precision}`;
|
|
1349
|
+
const cached = numberCache.get(cacheKey);
|
|
1350
|
+
if (cached !== undefined) return cached;
|
|
1351
|
+
|
|
1308
1352
|
const parsed = parseFloat(num);
|
|
1309
1353
|
|
|
1310
1354
|
// Handle special cases
|
|
@@ -1316,11 +1360,13 @@ function minifyNumber(num, precision = 3) {
|
|
|
1316
1360
|
const fixed = parsed.toFixed(precision);
|
|
1317
1361
|
const trimmed = fixed.replace(/\.?0+$/, '');
|
|
1318
1362
|
|
|
1319
|
-
|
|
1363
|
+
const result = trimmed || '0';
|
|
1364
|
+
numberCache.set(cacheKey, result);
|
|
1365
|
+
return result;
|
|
1320
1366
|
}
|
|
1321
1367
|
|
|
1322
1368
|
/**
|
|
1323
|
-
* Minify SVG path data by reducing numeric precision
|
|
1369
|
+
* Minify SVG path data by reducing numeric precision and removing unnecessary spaces
|
|
1324
1370
|
* @param {string} pathData - SVG path data string
|
|
1325
1371
|
* @param {number} precision - Decimal precision for coordinates
|
|
1326
1372
|
* @returns {string} Minified path data
|
|
@@ -1328,11 +1374,25 @@ function minifyNumber(num, precision = 3) {
|
|
|
1328
1374
|
function minifyPathData(pathData, precision = 3) {
|
|
1329
1375
|
if (!pathData || typeof pathData !== 'string') return pathData;
|
|
1330
1376
|
|
|
1331
|
-
//
|
|
1332
|
-
|
|
1333
|
-
return pathData.replace(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g, (match) => {
|
|
1377
|
+
// First, minify all numbers
|
|
1378
|
+
let result = pathData.replace(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g, (match) => {
|
|
1334
1379
|
return minifyNumber(match, precision);
|
|
1335
1380
|
});
|
|
1381
|
+
|
|
1382
|
+
// Remove unnecessary spaces around path commands
|
|
1383
|
+
// Safe to remove space after a command letter when it’s followed by a number (which may be negative)
|
|
1384
|
+
// M 10 20 → M10 20, L -5 -3 → L-5-3
|
|
1385
|
+
result = result.replace(/([MLHVCSQTAZmlhvcsqtaz])\s+(?=-?\d)/g, '$1');
|
|
1386
|
+
|
|
1387
|
+
// Safe to remove space before command letter when preceded by a number
|
|
1388
|
+
// 0 L → 0L, 20 M → 20M
|
|
1389
|
+
result = result.replace(/(\d)\s+([MLHVCSQTAZmlhvcsqtaz])/g, '$1$2');
|
|
1390
|
+
|
|
1391
|
+
// Safe to remove space before negative number when preceded by a number
|
|
1392
|
+
// 10 -20 → 10-20 (numbers are separated by the minus sign)
|
|
1393
|
+
result = result.replace(/(\d)\s+(-\d)/g, '$1$2');
|
|
1394
|
+
|
|
1395
|
+
return result;
|
|
1336
1396
|
}
|
|
1337
1397
|
|
|
1338
1398
|
/**
|
|
@@ -1361,27 +1421,49 @@ function minifyAttributeWhitespace(value) {
|
|
|
1361
1421
|
}
|
|
1362
1422
|
|
|
1363
1423
|
/**
|
|
1364
|
-
* Minify color values (hex shortening, rgb to hex conversion)
|
|
1424
|
+
* Minify color values (hex shortening, rgb to hex conversion, named colors)
|
|
1425
|
+
* Only processes simple color values; preserves case-sensitive references like `url(#id)`
|
|
1365
1426
|
* @param {string} color - Color value to minify
|
|
1366
1427
|
* @returns {string} Minified color value
|
|
1367
1428
|
*/
|
|
1368
1429
|
function minifyColor(color) {
|
|
1369
1430
|
if (!color || typeof color !== 'string') return color;
|
|
1370
1431
|
|
|
1371
|
-
const trimmed = color.trim()
|
|
1432
|
+
const trimmed = color.trim();
|
|
1433
|
+
|
|
1434
|
+
// Don’t process values that aren’t simple colors (preserve case-sensitive references)
|
|
1435
|
+
// `url(#id)`, `var(--name)`, `inherit`, `currentColor`, etc.
|
|
1436
|
+
if (trimmed.includes('url(') || trimmed.includes('var(') ||
|
|
1437
|
+
trimmed === 'inherit' || trimmed === 'currentColor') {
|
|
1438
|
+
return trimmed;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// Now safe to lowercase for color matching
|
|
1442
|
+
const lower = trimmed.toLowerCase();
|
|
1372
1443
|
|
|
1373
1444
|
// Shorten 6-digit hex to 3-digit when possible
|
|
1374
1445
|
// #aabbcc → #abc, #000000 → #000
|
|
1375
|
-
const hexMatch =
|
|
1446
|
+
const hexMatch = lower.match(/^#([0-9a-f]{6})$/);
|
|
1376
1447
|
if (hexMatch) {
|
|
1377
1448
|
const hex = hexMatch[1];
|
|
1378
1449
|
if (hex[0] === hex[1] && hex[2] === hex[3] && hex[4] === hex[5]) {
|
|
1379
|
-
|
|
1450
|
+
const shortened = '#' + hex[0] + hex[2] + hex[4];
|
|
1451
|
+
// Try to use named color if shorter
|
|
1452
|
+
return NAMED_COLORS[shortened] || shortened;
|
|
1380
1453
|
}
|
|
1454
|
+
// Can’t shorten, but check for named color
|
|
1455
|
+
return NAMED_COLORS[lower] || lower;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// Match 3-digit hex colors
|
|
1459
|
+
const hex3Match = lower.match(/^#[0-9a-f]{3}$/);
|
|
1460
|
+
if (hex3Match) {
|
|
1461
|
+
// Check if there’s a shorter named color
|
|
1462
|
+
return NAMED_COLORS[lower] || lower;
|
|
1381
1463
|
}
|
|
1382
1464
|
|
|
1383
1465
|
// Convert rgb(255,255,255) to hex
|
|
1384
|
-
const rgbMatch =
|
|
1466
|
+
const rgbMatch = lower.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
|
|
1385
1467
|
if (rgbMatch) {
|
|
1386
1468
|
const r = parseInt(rgbMatch[1], 10);
|
|
1387
1469
|
const g = parseInt(rgbMatch[2], 10);
|
|
@@ -1396,13 +1478,15 @@ function minifyColor(color) {
|
|
|
1396
1478
|
|
|
1397
1479
|
// Try to shorten if possible
|
|
1398
1480
|
if (hexColor[1] === hexColor[2] && hexColor[3] === hexColor[4] && hexColor[5] === hexColor[6]) {
|
|
1399
|
-
|
|
1481
|
+
const shortened = '#' + hexColor[1] + hexColor[3] + hexColor[5];
|
|
1482
|
+
return NAMED_COLORS[shortened] || shortened;
|
|
1400
1483
|
}
|
|
1401
|
-
return hexColor;
|
|
1484
|
+
return NAMED_COLORS[hexColor] || hexColor;
|
|
1402
1485
|
}
|
|
1403
1486
|
}
|
|
1404
1487
|
|
|
1405
|
-
return
|
|
1488
|
+
// Not a recognized color format, return as-is (preserves case)
|
|
1489
|
+
return trimmed;
|
|
1406
1490
|
}
|
|
1407
1491
|
|
|
1408
1492
|
// Attributes that contain numeric sequences or path data
|
|
@@ -1432,13 +1516,58 @@ const COLOR_ATTRS = new Set([
|
|
|
1432
1516
|
'lighting-color'
|
|
1433
1517
|
]);
|
|
1434
1518
|
|
|
1519
|
+
// Pre-compiled regexes for identity transform detection (compiled once at module load)
|
|
1520
|
+
// Separator pattern: Accepts comma with optional spaces or one or more spaces
|
|
1521
|
+
const SEP = '(?:\\s*,\\s*|\\s+)';
|
|
1522
|
+
|
|
1523
|
+
// `translate(0)`, `translate(0,0)`, `translate(0 0)` (matches 0, 0.0, 0.00, etc.)
|
|
1524
|
+
const IDENTITY_TRANSLATE_RE = new RegExp(`^translate\\s*\\(\\s*0(?:\\.0+)?\\s*(?:${SEP}0(?:\\.0+)?\\s*)?\\)$`, 'i');
|
|
1525
|
+
|
|
1526
|
+
// `scale(1)`, `scale(1,1)`, `scale(1 1)` (matches 1, 1.0, 1.00, etc.)
|
|
1527
|
+
const IDENTITY_SCALE_RE = new RegExp(`^scale\\s*\\(\\s*1(?:\\.0+)?\\s*(?:${SEP}1(?:\\.0+)?\\s*)?\\)$`, 'i');
|
|
1528
|
+
|
|
1529
|
+
// `rotate(0)`, `rotate(0 cx cy)`, `rotate(0, cx, cy)` (matches 0, 0.0, 0.00, etc.)
|
|
1530
|
+
// Note: `cx` and `cy` must be valid numbers if present
|
|
1531
|
+
const IDENTITY_ROTATE_RE = new RegExp(`^rotate\\s*\\(\\s*0(?:\\.0+)?\\s*(?:${SEP}-?\\d+(?:\\.\\d+)?${SEP}-?\\d+(?:\\.\\d+)?)?\\s*\\)$`, 'i');
|
|
1532
|
+
|
|
1533
|
+
// `skewX(0)`, `skewY(0)` (matches 0, 0.0, 0.00, etc.)
|
|
1534
|
+
const IDENTITY_SKEW_RE = /^skew[XY]\s*\(\s*0(?:\.0+)?\s*\)$/i;
|
|
1535
|
+
|
|
1536
|
+
// `matrix(1,0,0,1,0,0)`, `matrix(1 0 0 1 0 0)`—identity matrix (matches 1.0/0.0 variants)
|
|
1537
|
+
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');
|
|
1538
|
+
|
|
1539
|
+
/**
|
|
1540
|
+
* Check if a transform attribute has no effect (identity transform)
|
|
1541
|
+
* @param {string} transform - Transform attribute value
|
|
1542
|
+
* @returns {boolean} True if transform is an identity (has no effect)
|
|
1543
|
+
*/
|
|
1544
|
+
function isIdentityTransform(transform) {
|
|
1545
|
+
if (!transform || typeof transform !== 'string') return false;
|
|
1546
|
+
|
|
1547
|
+
const trimmed = transform.trim();
|
|
1548
|
+
|
|
1549
|
+
// Check for common identity transforms using pre-compiled regexes
|
|
1550
|
+
return IDENTITY_TRANSLATE_RE.test(trimmed) ||
|
|
1551
|
+
IDENTITY_SCALE_RE.test(trimmed) ||
|
|
1552
|
+
IDENTITY_ROTATE_RE.test(trimmed) ||
|
|
1553
|
+
IDENTITY_SKEW_RE.test(trimmed) ||
|
|
1554
|
+
IDENTITY_MATRIX_RE.test(trimmed);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1435
1557
|
/**
|
|
1436
1558
|
* Check if an attribute should be removed based on default value
|
|
1559
|
+
* @param {string} tag - Element tag name (e.g., `svg`, `rect`, `path`)
|
|
1437
1560
|
* @param {string} name - Attribute name
|
|
1438
1561
|
* @param {string} value - Attribute value
|
|
1439
1562
|
* @returns {boolean} True if attribute can be removed
|
|
1440
1563
|
*/
|
|
1441
|
-
function isDefaultAttribute(name, value) {
|
|
1564
|
+
function isDefaultAttribute(tag, name, value) {
|
|
1565
|
+
// Special case: `overflow="visible"` is unsafe for root `<svg>` element
|
|
1566
|
+
// Root SVG may need explicit `overflow="visible"` to show clipped content
|
|
1567
|
+
if (name === 'overflow' && value === 'visible') {
|
|
1568
|
+
return tag !== 'svg'; // Only remove for non-root SVG elements
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1442
1571
|
const checker = SVG_DEFAULT_ATTRS[name];
|
|
1443
1572
|
if (!checker) return false;
|
|
1444
1573
|
|
|
@@ -1489,17 +1618,23 @@ function minifySVGAttributeValue(name, value, options = {}) {
|
|
|
1489
1618
|
|
|
1490
1619
|
/**
|
|
1491
1620
|
* Check if an SVG attribute can be removed
|
|
1621
|
+
* @param {string} tag - Element tag name (e.g., `svg`, `rect`, `path`)
|
|
1492
1622
|
* @param {string} name - Attribute name
|
|
1493
1623
|
* @param {string} value - Attribute value
|
|
1494
1624
|
* @param {Object} options - Minification options
|
|
1495
1625
|
* @returns {boolean} True if attribute should be removed
|
|
1496
1626
|
*/
|
|
1497
|
-
function shouldRemoveSVGAttribute(name, value, options = {}) {
|
|
1627
|
+
function shouldRemoveSVGAttribute(tag, name, value, options = {}) {
|
|
1498
1628
|
const { removeDefaults = true } = options;
|
|
1499
1629
|
|
|
1500
1630
|
if (!removeDefaults) return false;
|
|
1501
1631
|
|
|
1502
|
-
|
|
1632
|
+
// Check for identity transforms
|
|
1633
|
+
if (name === 'transform' && isIdentityTransform(value)) {
|
|
1634
|
+
return true;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
return isDefaultAttribute(tag, name, value);
|
|
1503
1638
|
}
|
|
1504
1639
|
|
|
1505
1640
|
/**
|
|
@@ -2067,7 +2202,15 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
2067
2202
|
// Apply early whitespace normalization if enabled
|
|
2068
2203
|
// Preserves special spaces (non-breaking space, hair space, etc.) for consistency with `collapseWhitespace`
|
|
2069
2204
|
if (options.collapseAttributeWhitespace) {
|
|
2070
|
-
|
|
2205
|
+
// Single-pass: Trim leading/trailing whitespace and collapse internal whitespace to single space
|
|
2206
|
+
attrValue = attrValue.replace(/^[ \n\r\t\f]+|[ \n\r\t\f]+$|[ \n\r\t\f]+/g, function(match, offset, str) {
|
|
2207
|
+
// Leading whitespace (`offset === 0`)
|
|
2208
|
+
if (offset === 0) return '';
|
|
2209
|
+
// Trailing whitespace (match ends at string end)
|
|
2210
|
+
if (offset + match.length === str.length) return '';
|
|
2211
|
+
// Internal whitespace
|
|
2212
|
+
return ' ';
|
|
2213
|
+
});
|
|
2071
2214
|
}
|
|
2072
2215
|
|
|
2073
2216
|
if (isEventAttribute(attrName, options)) {
|
|
@@ -2241,7 +2384,7 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
2241
2384
|
(options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
|
|
2242
2385
|
attrName === 'type' && isStyleLinkTypeAttribute(attrValue)) ||
|
|
2243
2386
|
(options.insideSVG && options.minifySVG &&
|
|
2244
|
-
shouldRemoveSVGAttribute(attrName, attrValue, options.minifySVG))) {
|
|
2387
|
+
shouldRemoveSVGAttribute(tag, attrName, attrValue, options.minifySVG))) {
|
|
2245
2388
|
return;
|
|
2246
2389
|
}
|
|
2247
2390
|
|
|
@@ -3236,37 +3236,29 @@ class Sorter {
|
|
|
3236
3236
|
for (let i = 0, len = this.keys.length; i < len; i++) {
|
|
3237
3237
|
const token = this.keys[i];
|
|
3238
3238
|
|
|
3239
|
-
//
|
|
3240
|
-
|
|
3239
|
+
// Single pass: Count matches and collect non-matches
|
|
3240
|
+
let matchCount = 0;
|
|
3241
|
+
const others = [];
|
|
3242
|
+
|
|
3241
3243
|
for (let j = fromIndex; j < tokens.length; j++) {
|
|
3242
3244
|
if (tokens[j] === token) {
|
|
3243
|
-
|
|
3245
|
+
matchCount++;
|
|
3246
|
+
} else {
|
|
3247
|
+
others.push(tokens[j]);
|
|
3244
3248
|
}
|
|
3245
3249
|
}
|
|
3246
3250
|
|
|
3247
|
-
if (
|
|
3248
|
-
//
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
for (let j = 0; j < positions.length; j++) {
|
|
3253
|
-
result.push(token);
|
|
3251
|
+
if (matchCount > 0) {
|
|
3252
|
+
// Rebuild: `matchCount` instances of token first, then others
|
|
3253
|
+
let writeIdx = fromIndex;
|
|
3254
|
+
for (let j = 0; j < matchCount; j++) {
|
|
3255
|
+
tokens[writeIdx++] = token;
|
|
3254
3256
|
}
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
const posSet = new Set(positions);
|
|
3258
|
-
for (let j = fromIndex; j < tokens.length; j++) {
|
|
3259
|
-
if (!posSet.has(j)) {
|
|
3260
|
-
result.push(tokens[j]);
|
|
3261
|
-
}
|
|
3257
|
+
for (let j = 0; j < others.length; j++) {
|
|
3258
|
+
tokens[writeIdx++] = others[j];
|
|
3262
3259
|
}
|
|
3263
3260
|
|
|
3264
|
-
|
|
3265
|
-
for (let j = 0; j < result.length; j++) {
|
|
3266
|
-
tokens[fromIndex + j] = result[j];
|
|
3267
|
-
}
|
|
3268
|
-
|
|
3269
|
-
const newFromIndex = fromIndex + positions.length;
|
|
3261
|
+
const newFromIndex = fromIndex + matchCount;
|
|
3270
3262
|
return this.sorterMap.get(token).sort(tokens, newFromIndex);
|
|
3271
3263
|
}
|
|
3272
3264
|
}
|
|
@@ -6402,8 +6394,31 @@ async function processScript(text, options, currentAttrs, minifyHTML) {
|
|
|
6402
6394
|
* - Numeric precision reduction for coordinates and path data
|
|
6403
6395
|
* - Whitespace removal in attribute values (numeric sequences)
|
|
6404
6396
|
* - Default attribute removal (safe, well-documented defaults)
|
|
6405
|
-
* - Color minification (hex shortening, rgb() to hex)
|
|
6397
|
+
* - Color minification (hex shortening, rgb() to hex, named colors)
|
|
6398
|
+
* - Identity transform removal
|
|
6399
|
+
* - Path data space optimization
|
|
6400
|
+
*/
|
|
6401
|
+
|
|
6402
|
+
|
|
6403
|
+
// Cache for minified numbers
|
|
6404
|
+
const numberCache = new LRU(100);
|
|
6405
|
+
|
|
6406
|
+
/**
|
|
6407
|
+
* Named colors that are shorter than their hex equivalents
|
|
6408
|
+
* Only includes cases where using the name saves bytes
|
|
6406
6409
|
*/
|
|
6410
|
+
const NAMED_COLORS = {
|
|
6411
|
+
'#f00': 'red', // #f00 (4) → red (3), saves 1
|
|
6412
|
+
'#c0c0c0': 'silver', // #c0c0c0 (7) → silver (6), saves 1
|
|
6413
|
+
'#808080': 'gray', // #808080 (7) → gray (4), saves 3
|
|
6414
|
+
'#800000': 'maroon', // #800000 (7) → maroon (6), saves 1
|
|
6415
|
+
'#808000': 'olive', // #808000 (7) → olive (5), saves 2
|
|
6416
|
+
'#008000': 'green', // #008000 (7) → green (5), saves 2
|
|
6417
|
+
'#800080': 'purple', // #800080 (7) → purple (6), saves 1
|
|
6418
|
+
'#008080': 'teal', // #008080 (7) → teal (4), saves 3
|
|
6419
|
+
'#000080': 'navy', // #000080 (7) → navy (4), saves 3
|
|
6420
|
+
'#ffa500': 'orange' // #ffa500 (7) → orange (6), saves 1
|
|
6421
|
+
};
|
|
6407
6422
|
|
|
6408
6423
|
/**
|
|
6409
6424
|
* Default SVG attribute values that can be safely removed
|
|
@@ -6437,7 +6452,22 @@ const SVG_DEFAULT_ATTRS = {
|
|
|
6437
6452
|
opacity: value => value === '1',
|
|
6438
6453
|
visibility: value => value === 'visible',
|
|
6439
6454
|
display: value => value === 'inline',
|
|
6440
|
-
|
|
6455
|
+
// Note: Overflow handled especially in `isDefaultAttribute` (not safe for root `<svg>`)
|
|
6456
|
+
|
|
6457
|
+
// Clipping and masking defaults
|
|
6458
|
+
'clip-rule': value => value === 'nonzero',
|
|
6459
|
+
'clip-path': value => value === 'none',
|
|
6460
|
+
mask: value => value === 'none',
|
|
6461
|
+
|
|
6462
|
+
// Marker defaults
|
|
6463
|
+
'marker-start': value => value === 'none',
|
|
6464
|
+
'marker-mid': value => value === 'none',
|
|
6465
|
+
'marker-end': value => value === 'none',
|
|
6466
|
+
|
|
6467
|
+
// Filter and color defaults
|
|
6468
|
+
filter: value => value === 'none',
|
|
6469
|
+
'color-interpolation': value => value === 'sRGB',
|
|
6470
|
+
'color-interpolation-filters': value => value === 'linearRGB'
|
|
6441
6471
|
};
|
|
6442
6472
|
|
|
6443
6473
|
/**
|
|
@@ -6447,6 +6477,20 @@ const SVG_DEFAULT_ATTRS = {
|
|
|
6447
6477
|
* @returns {string} Minified numeric string
|
|
6448
6478
|
*/
|
|
6449
6479
|
function minifyNumber(num, precision = 3) {
|
|
6480
|
+
// Fast path for common values (avoids parsing and caching)
|
|
6481
|
+
if (num === '0' || num === '1') return num;
|
|
6482
|
+
// Common decimal variants that tools export
|
|
6483
|
+
if (num === '0.0' || num === '0.00' || num === '0.000') return '0';
|
|
6484
|
+
if (num === '1.0' || num === '1.00' || num === '1.000') return '1';
|
|
6485
|
+
|
|
6486
|
+
// Check cache
|
|
6487
|
+
// (Note: uses input string as key, so “0.0000” and “0.00000” create separate entries.
|
|
6488
|
+
// This is intentional to avoid parsing overhead.
|
|
6489
|
+
// Real-world SVG files from export tools typically use consistent formats.)
|
|
6490
|
+
const cacheKey = `${num}:${precision}`;
|
|
6491
|
+
const cached = numberCache.get(cacheKey);
|
|
6492
|
+
if (cached !== undefined) return cached;
|
|
6493
|
+
|
|
6450
6494
|
const parsed = parseFloat(num);
|
|
6451
6495
|
|
|
6452
6496
|
// Handle special cases
|
|
@@ -6458,11 +6502,13 @@ function minifyNumber(num, precision = 3) {
|
|
|
6458
6502
|
const fixed = parsed.toFixed(precision);
|
|
6459
6503
|
const trimmed = fixed.replace(/\.?0+$/, '');
|
|
6460
6504
|
|
|
6461
|
-
|
|
6505
|
+
const result = trimmed || '0';
|
|
6506
|
+
numberCache.set(cacheKey, result);
|
|
6507
|
+
return result;
|
|
6462
6508
|
}
|
|
6463
6509
|
|
|
6464
6510
|
/**
|
|
6465
|
-
* Minify SVG path data by reducing numeric precision
|
|
6511
|
+
* Minify SVG path data by reducing numeric precision and removing unnecessary spaces
|
|
6466
6512
|
* @param {string} pathData - SVG path data string
|
|
6467
6513
|
* @param {number} precision - Decimal precision for coordinates
|
|
6468
6514
|
* @returns {string} Minified path data
|
|
@@ -6470,11 +6516,25 @@ function minifyNumber(num, precision = 3) {
|
|
|
6470
6516
|
function minifyPathData(pathData, precision = 3) {
|
|
6471
6517
|
if (!pathData || typeof pathData !== 'string') return pathData;
|
|
6472
6518
|
|
|
6473
|
-
//
|
|
6474
|
-
|
|
6475
|
-
return pathData.replace(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g, (match) => {
|
|
6519
|
+
// First, minify all numbers
|
|
6520
|
+
let result = pathData.replace(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g, (match) => {
|
|
6476
6521
|
return minifyNumber(match, precision);
|
|
6477
6522
|
});
|
|
6523
|
+
|
|
6524
|
+
// Remove unnecessary spaces around path commands
|
|
6525
|
+
// Safe to remove space after a command letter when it’s followed by a number (which may be negative)
|
|
6526
|
+
// M 10 20 → M10 20, L -5 -3 → L-5-3
|
|
6527
|
+
result = result.replace(/([MLHVCSQTAZmlhvcsqtaz])\s+(?=-?\d)/g, '$1');
|
|
6528
|
+
|
|
6529
|
+
// Safe to remove space before command letter when preceded by a number
|
|
6530
|
+
// 0 L → 0L, 20 M → 20M
|
|
6531
|
+
result = result.replace(/(\d)\s+([MLHVCSQTAZmlhvcsqtaz])/g, '$1$2');
|
|
6532
|
+
|
|
6533
|
+
// Safe to remove space before negative number when preceded by a number
|
|
6534
|
+
// 10 -20 → 10-20 (numbers are separated by the minus sign)
|
|
6535
|
+
result = result.replace(/(\d)\s+(-\d)/g, '$1$2');
|
|
6536
|
+
|
|
6537
|
+
return result;
|
|
6478
6538
|
}
|
|
6479
6539
|
|
|
6480
6540
|
/**
|
|
@@ -6503,27 +6563,49 @@ function minifyAttributeWhitespace(value) {
|
|
|
6503
6563
|
}
|
|
6504
6564
|
|
|
6505
6565
|
/**
|
|
6506
|
-
* Minify color values (hex shortening, rgb to hex conversion)
|
|
6566
|
+
* Minify color values (hex shortening, rgb to hex conversion, named colors)
|
|
6567
|
+
* Only processes simple color values; preserves case-sensitive references like `url(#id)`
|
|
6507
6568
|
* @param {string} color - Color value to minify
|
|
6508
6569
|
* @returns {string} Minified color value
|
|
6509
6570
|
*/
|
|
6510
6571
|
function minifyColor(color) {
|
|
6511
6572
|
if (!color || typeof color !== 'string') return color;
|
|
6512
6573
|
|
|
6513
|
-
const trimmed = color.trim()
|
|
6574
|
+
const trimmed = color.trim();
|
|
6575
|
+
|
|
6576
|
+
// Don’t process values that aren’t simple colors (preserve case-sensitive references)
|
|
6577
|
+
// `url(#id)`, `var(--name)`, `inherit`, `currentColor`, etc.
|
|
6578
|
+
if (trimmed.includes('url(') || trimmed.includes('var(') ||
|
|
6579
|
+
trimmed === 'inherit' || trimmed === 'currentColor') {
|
|
6580
|
+
return trimmed;
|
|
6581
|
+
}
|
|
6582
|
+
|
|
6583
|
+
// Now safe to lowercase for color matching
|
|
6584
|
+
const lower = trimmed.toLowerCase();
|
|
6514
6585
|
|
|
6515
6586
|
// Shorten 6-digit hex to 3-digit when possible
|
|
6516
6587
|
// #aabbcc → #abc, #000000 → #000
|
|
6517
|
-
const hexMatch =
|
|
6588
|
+
const hexMatch = lower.match(/^#([0-9a-f]{6})$/);
|
|
6518
6589
|
if (hexMatch) {
|
|
6519
6590
|
const hex = hexMatch[1];
|
|
6520
6591
|
if (hex[0] === hex[1] && hex[2] === hex[3] && hex[4] === hex[5]) {
|
|
6521
|
-
|
|
6592
|
+
const shortened = '#' + hex[0] + hex[2] + hex[4];
|
|
6593
|
+
// Try to use named color if shorter
|
|
6594
|
+
return NAMED_COLORS[shortened] || shortened;
|
|
6522
6595
|
}
|
|
6596
|
+
// Can’t shorten, but check for named color
|
|
6597
|
+
return NAMED_COLORS[lower] || lower;
|
|
6598
|
+
}
|
|
6599
|
+
|
|
6600
|
+
// Match 3-digit hex colors
|
|
6601
|
+
const hex3Match = lower.match(/^#[0-9a-f]{3}$/);
|
|
6602
|
+
if (hex3Match) {
|
|
6603
|
+
// Check if there’s a shorter named color
|
|
6604
|
+
return NAMED_COLORS[lower] || lower;
|
|
6523
6605
|
}
|
|
6524
6606
|
|
|
6525
6607
|
// Convert rgb(255,255,255) to hex
|
|
6526
|
-
const rgbMatch =
|
|
6608
|
+
const rgbMatch = lower.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
|
|
6527
6609
|
if (rgbMatch) {
|
|
6528
6610
|
const r = parseInt(rgbMatch[1], 10);
|
|
6529
6611
|
const g = parseInt(rgbMatch[2], 10);
|
|
@@ -6538,13 +6620,15 @@ function minifyColor(color) {
|
|
|
6538
6620
|
|
|
6539
6621
|
// Try to shorten if possible
|
|
6540
6622
|
if (hexColor[1] === hexColor[2] && hexColor[3] === hexColor[4] && hexColor[5] === hexColor[6]) {
|
|
6541
|
-
|
|
6623
|
+
const shortened = '#' + hexColor[1] + hexColor[3] + hexColor[5];
|
|
6624
|
+
return NAMED_COLORS[shortened] || shortened;
|
|
6542
6625
|
}
|
|
6543
|
-
return hexColor;
|
|
6626
|
+
return NAMED_COLORS[hexColor] || hexColor;
|
|
6544
6627
|
}
|
|
6545
6628
|
}
|
|
6546
6629
|
|
|
6547
|
-
return
|
|
6630
|
+
// Not a recognized color format, return as-is (preserves case)
|
|
6631
|
+
return trimmed;
|
|
6548
6632
|
}
|
|
6549
6633
|
|
|
6550
6634
|
// Attributes that contain numeric sequences or path data
|
|
@@ -6574,13 +6658,58 @@ const COLOR_ATTRS = new Set([
|
|
|
6574
6658
|
'lighting-color'
|
|
6575
6659
|
]);
|
|
6576
6660
|
|
|
6661
|
+
// Pre-compiled regexes for identity transform detection (compiled once at module load)
|
|
6662
|
+
// Separator pattern: Accepts comma with optional spaces or one or more spaces
|
|
6663
|
+
const SEP = '(?:\\s*,\\s*|\\s+)';
|
|
6664
|
+
|
|
6665
|
+
// `translate(0)`, `translate(0,0)`, `translate(0 0)` (matches 0, 0.0, 0.00, etc.)
|
|
6666
|
+
const IDENTITY_TRANSLATE_RE = new RegExp(`^translate\\s*\\(\\s*0(?:\\.0+)?\\s*(?:${SEP}0(?:\\.0+)?\\s*)?\\)$`, 'i');
|
|
6667
|
+
|
|
6668
|
+
// `scale(1)`, `scale(1,1)`, `scale(1 1)` (matches 1, 1.0, 1.00, etc.)
|
|
6669
|
+
const IDENTITY_SCALE_RE = new RegExp(`^scale\\s*\\(\\s*1(?:\\.0+)?\\s*(?:${SEP}1(?:\\.0+)?\\s*)?\\)$`, 'i');
|
|
6670
|
+
|
|
6671
|
+
// `rotate(0)`, `rotate(0 cx cy)`, `rotate(0, cx, cy)` (matches 0, 0.0, 0.00, etc.)
|
|
6672
|
+
// Note: `cx` and `cy` must be valid numbers if present
|
|
6673
|
+
const IDENTITY_ROTATE_RE = new RegExp(`^rotate\\s*\\(\\s*0(?:\\.0+)?\\s*(?:${SEP}-?\\d+(?:\\.\\d+)?${SEP}-?\\d+(?:\\.\\d+)?)?\\s*\\)$`, 'i');
|
|
6674
|
+
|
|
6675
|
+
// `skewX(0)`, `skewY(0)` (matches 0, 0.0, 0.00, etc.)
|
|
6676
|
+
const IDENTITY_SKEW_RE = /^skew[XY]\s*\(\s*0(?:\.0+)?\s*\)$/i;
|
|
6677
|
+
|
|
6678
|
+
// `matrix(1,0,0,1,0,0)`, `matrix(1 0 0 1 0 0)`—identity matrix (matches 1.0/0.0 variants)
|
|
6679
|
+
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');
|
|
6680
|
+
|
|
6681
|
+
/**
|
|
6682
|
+
* Check if a transform attribute has no effect (identity transform)
|
|
6683
|
+
* @param {string} transform - Transform attribute value
|
|
6684
|
+
* @returns {boolean} True if transform is an identity (has no effect)
|
|
6685
|
+
*/
|
|
6686
|
+
function isIdentityTransform(transform) {
|
|
6687
|
+
if (!transform || typeof transform !== 'string') return false;
|
|
6688
|
+
|
|
6689
|
+
const trimmed = transform.trim();
|
|
6690
|
+
|
|
6691
|
+
// Check for common identity transforms using pre-compiled regexes
|
|
6692
|
+
return IDENTITY_TRANSLATE_RE.test(trimmed) ||
|
|
6693
|
+
IDENTITY_SCALE_RE.test(trimmed) ||
|
|
6694
|
+
IDENTITY_ROTATE_RE.test(trimmed) ||
|
|
6695
|
+
IDENTITY_SKEW_RE.test(trimmed) ||
|
|
6696
|
+
IDENTITY_MATRIX_RE.test(trimmed);
|
|
6697
|
+
}
|
|
6698
|
+
|
|
6577
6699
|
/**
|
|
6578
6700
|
* Check if an attribute should be removed based on default value
|
|
6701
|
+
* @param {string} tag - Element tag name (e.g., `svg`, `rect`, `path`)
|
|
6579
6702
|
* @param {string} name - Attribute name
|
|
6580
6703
|
* @param {string} value - Attribute value
|
|
6581
6704
|
* @returns {boolean} True if attribute can be removed
|
|
6582
6705
|
*/
|
|
6583
|
-
function isDefaultAttribute(name, value) {
|
|
6706
|
+
function isDefaultAttribute(tag, name, value) {
|
|
6707
|
+
// Special case: `overflow="visible"` is unsafe for root `<svg>` element
|
|
6708
|
+
// Root SVG may need explicit `overflow="visible"` to show clipped content
|
|
6709
|
+
if (name === 'overflow' && value === 'visible') {
|
|
6710
|
+
return tag !== 'svg'; // Only remove for non-root SVG elements
|
|
6711
|
+
}
|
|
6712
|
+
|
|
6584
6713
|
const checker = SVG_DEFAULT_ATTRS[name];
|
|
6585
6714
|
if (!checker) return false;
|
|
6586
6715
|
|
|
@@ -6631,17 +6760,23 @@ function minifySVGAttributeValue(name, value, options = {}) {
|
|
|
6631
6760
|
|
|
6632
6761
|
/**
|
|
6633
6762
|
* Check if an SVG attribute can be removed
|
|
6763
|
+
* @param {string} tag - Element tag name (e.g., `svg`, `rect`, `path`)
|
|
6634
6764
|
* @param {string} name - Attribute name
|
|
6635
6765
|
* @param {string} value - Attribute value
|
|
6636
6766
|
* @param {Object} options - Minification options
|
|
6637
6767
|
* @returns {boolean} True if attribute should be removed
|
|
6638
6768
|
*/
|
|
6639
|
-
function shouldRemoveSVGAttribute(name, value, options = {}) {
|
|
6769
|
+
function shouldRemoveSVGAttribute(tag, name, value, options = {}) {
|
|
6640
6770
|
const { removeDefaults = true } = options;
|
|
6641
6771
|
|
|
6642
6772
|
if (!removeDefaults) return false;
|
|
6643
6773
|
|
|
6644
|
-
|
|
6774
|
+
// Check for identity transforms
|
|
6775
|
+
if (name === 'transform' && isIdentityTransform(value)) {
|
|
6776
|
+
return true;
|
|
6777
|
+
}
|
|
6778
|
+
|
|
6779
|
+
return isDefaultAttribute(tag, name, value);
|
|
6645
6780
|
}
|
|
6646
6781
|
|
|
6647
6782
|
/**
|
|
@@ -7209,7 +7344,15 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
7209
7344
|
// Apply early whitespace normalization if enabled
|
|
7210
7345
|
// Preserves special spaces (non-breaking space, hair space, etc.) for consistency with `collapseWhitespace`
|
|
7211
7346
|
if (options.collapseAttributeWhitespace) {
|
|
7212
|
-
|
|
7347
|
+
// Single-pass: Trim leading/trailing whitespace and collapse internal whitespace to single space
|
|
7348
|
+
attrValue = attrValue.replace(/^[ \n\r\t\f]+|[ \n\r\t\f]+$|[ \n\r\t\f]+/g, function(match, offset, str) {
|
|
7349
|
+
// Leading whitespace (`offset === 0`)
|
|
7350
|
+
if (offset === 0) return '';
|
|
7351
|
+
// Trailing whitespace (match ends at string end)
|
|
7352
|
+
if (offset + match.length === str.length) return '';
|
|
7353
|
+
// Internal whitespace
|
|
7354
|
+
return ' ';
|
|
7355
|
+
});
|
|
7213
7356
|
}
|
|
7214
7357
|
|
|
7215
7358
|
if (isEventAttribute(attrName, options)) {
|
|
@@ -7383,7 +7526,7 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
7383
7526
|
(options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
|
|
7384
7527
|
attrName === 'type' && isStyleLinkTypeAttribute(attrValue)) ||
|
|
7385
7528
|
(options.insideSVG && options.minifySVG &&
|
|
7386
|
-
shouldRemoveSVGAttribute(attrName, attrValue, options.minifySVG))) {
|
|
7529
|
+
shouldRemoveSVGAttribute(tag, attrName, attrValue, options.minifySVG))) {
|
|
7387
7530
|
return;
|
|
7388
7531
|
}
|
|
7389
7532
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attributes.d.ts","sourceRoot":"","sources":["../../../src/lib/attributes.js"],"names":[],"mappings":"AAuBA,yDAEC;AAED,mEAOC;AAED,uEAWC;AAED,8DAGC;AAED,4EAOC;AAED,mGAqBC;AAED,mEAGC;AAED,qEAGC;AAED,kEAWC;AAED,sEAGC;AAED,4DAWC;AAED,2EAEC;AAED,qEAaC;AAED,wEAUC;AAED,sEAUC;AAED,2EAEC;AAED,2DAEC;AAED,8DAUC;AAED,uEAUC;AAED,oGASC;AAED,4DAOC;AAID,
|
|
1
|
+
{"version":3,"file":"attributes.d.ts","sourceRoot":"","sources":["../../../src/lib/attributes.js"],"names":[],"mappings":"AAuBA,yDAEC;AAED,mEAOC;AAED,uEAWC;AAED,8DAGC;AAED,4EAOC;AAED,mGAqBC;AAED,mEAGC;AAED,qEAGC;AAED,kEAWC;AAED,sEAGC;AAED,4DAWC;AAED,2EAEC;AAED,qEAaC;AAED,wEAUC;AAED,sEAUC;AAED,2EAEC;AAED,2DAEC;AAED,8DAUC;AAED,uEAUC;AAED,oGASC;AAED,4DAOC;AAID,0IAkJC;AAsBD;;;;GAwCC;AAED,6GA4EC"}
|
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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokenchain.d.ts","sourceRoot":"","sources":["../../src/tokenchain.js"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"tokenchain.d.ts","sourceRoot":"","sources":["../../src/tokenchain.js"],"names":[],"mappings":";AAmCA;IAGI,mBAAoB;IAGtB,uBAOC;IAED,uBAiDC;CACF;AApGD;IACE,2CA+BC;CACF"}
|
package/package.json
CHANGED
package/src/lib/attributes.js
CHANGED
|
@@ -226,7 +226,15 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
226
226
|
// Apply early whitespace normalization if enabled
|
|
227
227
|
// Preserves special spaces (non-breaking space, hair space, etc.) for consistency with `collapseWhitespace`
|
|
228
228
|
if (options.collapseAttributeWhitespace) {
|
|
229
|
-
|
|
229
|
+
// Single-pass: Trim leading/trailing whitespace and collapse internal whitespace to single space
|
|
230
|
+
attrValue = attrValue.replace(/^[ \n\r\t\f]+|[ \n\r\t\f]+$|[ \n\r\t\f]+/g, function(match, offset, str) {
|
|
231
|
+
// Leading whitespace (`offset === 0`)
|
|
232
|
+
if (offset === 0) return '';
|
|
233
|
+
// Trailing whitespace (match ends at string end)
|
|
234
|
+
if (offset + match.length === str.length) return '';
|
|
235
|
+
// Internal whitespace
|
|
236
|
+
return ' ';
|
|
237
|
+
});
|
|
230
238
|
}
|
|
231
239
|
|
|
232
240
|
if (isEventAttribute(attrName, options)) {
|
|
@@ -400,7 +408,7 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
400
408
|
(options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
|
|
401
409
|
attrName === 'type' && isStyleLinkTypeAttribute(attrValue)) ||
|
|
402
410
|
(options.insideSVG && options.minifySVG &&
|
|
403
|
-
shouldRemoveSVGAttribute(attrName, attrValue, options.minifySVG))) {
|
|
411
|
+
shouldRemoveSVGAttribute(tag, attrName, attrValue, options.minifySVG))) {
|
|
404
412
|
return;
|
|
405
413
|
}
|
|
406
414
|
|
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
|
/**
|
package/src/tokenchain.js
CHANGED
|
@@ -3,37 +3,29 @@ class Sorter {
|
|
|
3
3
|
for (let i = 0, len = this.keys.length; i < len; i++) {
|
|
4
4
|
const token = this.keys[i];
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
|
|
6
|
+
// Single pass: Count matches and collect non-matches
|
|
7
|
+
let matchCount = 0;
|
|
8
|
+
const others = [];
|
|
9
|
+
|
|
8
10
|
for (let j = fromIndex; j < tokens.length; j++) {
|
|
9
11
|
if (tokens[j] === token) {
|
|
10
|
-
|
|
12
|
+
matchCount++;
|
|
13
|
+
} else {
|
|
14
|
+
others.push(tokens[j]);
|
|
11
15
|
}
|
|
12
16
|
}
|
|
13
17
|
|
|
14
|
-
if (
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
for (let j = 0; j < positions.length; j++) {
|
|
20
|
-
result.push(token);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Add other tokens, skipping positions where current token was
|
|
24
|
-
const posSet = new Set(positions);
|
|
25
|
-
for (let j = fromIndex; j < tokens.length; j++) {
|
|
26
|
-
if (!posSet.has(j)) {
|
|
27
|
-
result.push(tokens[j]);
|
|
28
|
-
}
|
|
18
|
+
if (matchCount > 0) {
|
|
19
|
+
// Rebuild: `matchCount` instances of token first, then others
|
|
20
|
+
let writeIdx = fromIndex;
|
|
21
|
+
for (let j = 0; j < matchCount; j++) {
|
|
22
|
+
tokens[writeIdx++] = token;
|
|
29
23
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
for (let j = 0; j < result.length; j++) {
|
|
33
|
-
tokens[fromIndex + j] = result[j];
|
|
24
|
+
for (let j = 0; j < others.length; j++) {
|
|
25
|
+
tokens[writeIdx++] = others[j];
|
|
34
26
|
}
|
|
35
27
|
|
|
36
|
-
const newFromIndex = fromIndex +
|
|
28
|
+
const newFromIndex = fromIndex + matchCount;
|
|
37
29
|
return this.sorterMap.get(token).sort(tokens, newFromIndex);
|
|
38
30
|
}
|
|
39
31
|
}
|