html-minifier-next 5.0.6 → 5.1.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 +48 -66
- package/cli.js +1 -0
- package/dist/htmlminifier.cjs +123 -450
- package/dist/htmlminifier.esm.bundle.js +123 -450
- package/dist/types/htmlminifier.d.ts +14 -10
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/lib/attributes.d.ts.map +1 -1
- package/dist/types/lib/constants.d.ts +0 -1
- package/dist/types/lib/constants.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts +3 -1
- package/dist/types/lib/options.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/htmlminifier.js +71 -8
- package/src/lib/attributes.js +1 -15
- package/src/lib/constants.js +0 -3
- package/src/lib/option-definitions.js +1 -1
- package/src/lib/options.js +51 -7
- package/dist/types/lib/svg.d.ts +0 -24
- package/dist/types/lib/svg.d.ts.map +0 -1
- package/src/lib/svg.js +0 -424
|
@@ -3647,7 +3647,6 @@ const RE_ESCAPE_LT = /</g;
|
|
|
3647
3647
|
const RE_ATTR_WS_CHECK = /[ \n\r\t\f]/;
|
|
3648
3648
|
const RE_ATTR_WS_COLLAPSE = /[ \n\r\t\f]+/g;
|
|
3649
3649
|
const RE_ATTR_WS_TRIM = /^[ \n\r\t\f]+|[ \n\r\t\f]+$/g;
|
|
3650
|
-
const RE_NUMERIC_VALUE = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
|
|
3651
3650
|
|
|
3652
3651
|
// Inline element sets for whitespace handling
|
|
3653
3652
|
|
|
@@ -4251,427 +4250,6 @@ async function processScript(text, options, currentAttrs, minifyHTML) {
|
|
|
4251
4250
|
return text;
|
|
4252
4251
|
}
|
|
4253
4252
|
|
|
4254
|
-
/**
|
|
4255
|
-
* Lightweight SVG optimizations:
|
|
4256
|
-
* - Numeric precision reduction for coordinates and path data
|
|
4257
|
-
* - Whitespace removal in attribute values (numeric sequences)
|
|
4258
|
-
* - Default attribute removal (safe, well-documented defaults)
|
|
4259
|
-
* - Color minification (hex shortening, rgb() to hex, named colors)
|
|
4260
|
-
* - Identity transform removal
|
|
4261
|
-
* - Path data space optimization
|
|
4262
|
-
*/
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
// Cache for minified numbers
|
|
4266
|
-
const numberCache = new LRU(100);
|
|
4267
|
-
|
|
4268
|
-
/**
|
|
4269
|
-
* Named colors that are shorter than their hex equivalents
|
|
4270
|
-
* Only includes cases where using the name saves bytes
|
|
4271
|
-
*/
|
|
4272
|
-
const NAMED_COLORS = {
|
|
4273
|
-
'#f00': 'red', // #f00 (4) → red (3), saves 1
|
|
4274
|
-
'#c0c0c0': 'silver', // #c0c0c0 (7) → silver (6), saves 1
|
|
4275
|
-
'#808080': 'gray', // #808080 (7) → gray (4), saves 3
|
|
4276
|
-
'#800000': 'maroon', // #800000 (7) → maroon (6), saves 1
|
|
4277
|
-
'#808000': 'olive', // #808000 (7) → olive (5), saves 2
|
|
4278
|
-
'#008000': 'green', // #008000 (7) → green (5), saves 2
|
|
4279
|
-
'#800080': 'purple', // #800080 (7) → purple (6), saves 1
|
|
4280
|
-
'#008080': 'teal', // #008080 (7) → teal (4), saves 3
|
|
4281
|
-
'#000080': 'navy', // #000080 (7) → navy (4), saves 3
|
|
4282
|
-
'#ffa500': 'orange' // #ffa500 (7) → orange (6), saves 1
|
|
4283
|
-
};
|
|
4284
|
-
|
|
4285
|
-
/**
|
|
4286
|
-
* Default SVG attribute values that can be safely removed
|
|
4287
|
-
* Only includes well-documented, widely-supported defaults
|
|
4288
|
-
*/
|
|
4289
|
-
const SVG_DEFAULT_ATTRS = {
|
|
4290
|
-
// Fill and stroke defaults
|
|
4291
|
-
fill: value => value === 'black' || value === '#000' || value === '#000000',
|
|
4292
|
-
'fill-opacity': value => value === '1',
|
|
4293
|
-
'fill-rule': value => value === 'nonzero',
|
|
4294
|
-
stroke: value => value === 'none',
|
|
4295
|
-
'stroke-dasharray': value => value === 'none',
|
|
4296
|
-
'stroke-dashoffset': value => value === '0',
|
|
4297
|
-
'stroke-linecap': value => value === 'butt',
|
|
4298
|
-
'stroke-linejoin': value => value === 'miter',
|
|
4299
|
-
'stroke-miterlimit': value => value === '4',
|
|
4300
|
-
'stroke-opacity': value => value === '1',
|
|
4301
|
-
'stroke-width': value => value === '1',
|
|
4302
|
-
|
|
4303
|
-
// Text and font defaults
|
|
4304
|
-
'font-family': value => value === 'inherit',
|
|
4305
|
-
'font-size': value => value === 'medium',
|
|
4306
|
-
'font-style': value => value === 'normal',
|
|
4307
|
-
'font-variant': value => value === 'normal',
|
|
4308
|
-
'font-weight': value => value === 'normal',
|
|
4309
|
-
'letter-spacing': value => value === 'normal',
|
|
4310
|
-
'text-decoration': value => value === 'none',
|
|
4311
|
-
'text-anchor': value => value === 'start',
|
|
4312
|
-
|
|
4313
|
-
// Other common defaults
|
|
4314
|
-
opacity: value => value === '1',
|
|
4315
|
-
visibility: value => value === 'visible',
|
|
4316
|
-
display: value => value === 'inline',
|
|
4317
|
-
// Note: Overflow handled especially in `isDefaultAttribute` (not safe for root `<svg>`)
|
|
4318
|
-
|
|
4319
|
-
// Clipping and masking defaults
|
|
4320
|
-
'clip-rule': value => value === 'nonzero',
|
|
4321
|
-
'clip-path': value => value === 'none',
|
|
4322
|
-
mask: value => value === 'none',
|
|
4323
|
-
|
|
4324
|
-
// Marker defaults
|
|
4325
|
-
'marker-start': value => value === 'none',
|
|
4326
|
-
'marker-mid': value => value === 'none',
|
|
4327
|
-
'marker-end': value => value === 'none',
|
|
4328
|
-
|
|
4329
|
-
// Filter and color defaults
|
|
4330
|
-
filter: value => value === 'none',
|
|
4331
|
-
'color-interpolation': value => value === 'sRGB',
|
|
4332
|
-
'color-interpolation-filters': value => value === 'linearRGB'
|
|
4333
|
-
};
|
|
4334
|
-
|
|
4335
|
-
/**
|
|
4336
|
-
* Minify numeric value by removing trailing zeros and unnecessary decimals
|
|
4337
|
-
* @param {string} num - Numeric string to minify
|
|
4338
|
-
* @param {number} precision - Maximum decimal places to keep
|
|
4339
|
-
* @returns {string} Minified numeric string
|
|
4340
|
-
*/
|
|
4341
|
-
function minifyNumber(num, precision = 3) {
|
|
4342
|
-
// Fast path for common values (avoids parsing and caching)
|
|
4343
|
-
if (num === '0' || num === '1') return num;
|
|
4344
|
-
// Common decimal variants that tools export
|
|
4345
|
-
if (num === '0.0' || num === '0.00' || num === '0.000') return '0';
|
|
4346
|
-
if (num === '1.0' || num === '1.00' || num === '1.000') return '1';
|
|
4347
|
-
|
|
4348
|
-
// Check cache
|
|
4349
|
-
// (Note: Uses input string as key, so “0.0000” and “0.00000” create separate entries.
|
|
4350
|
-
// This is intentional to avoid parsing overhead.
|
|
4351
|
-
// Real-world SVG files from export tools typically use consistent formats.)
|
|
4352
|
-
const cacheKey = `${num}:${precision}`;
|
|
4353
|
-
const cached = numberCache.get(cacheKey);
|
|
4354
|
-
if (cached !== undefined) return cached;
|
|
4355
|
-
|
|
4356
|
-
const parsed = parseFloat(num);
|
|
4357
|
-
|
|
4358
|
-
// Handle special cases
|
|
4359
|
-
if (isNaN(parsed)) return num;
|
|
4360
|
-
if (parsed === 0) return '0';
|
|
4361
|
-
if (!isFinite(parsed)) return num;
|
|
4362
|
-
|
|
4363
|
-
// Convert to fixed precision, then remove trailing zeros
|
|
4364
|
-
const fixed = parsed.toFixed(precision);
|
|
4365
|
-
const trimmed = fixed.replace(/\.?0+$/, '');
|
|
4366
|
-
|
|
4367
|
-
// Remove leading zero before decimal point (e.g., `0.5` → `.5`, `-0.3` → `-.3`)
|
|
4368
|
-
const result = (trimmed || '0').replace(/^(-?)0\./, '$1.');
|
|
4369
|
-
numberCache.set(cacheKey, result);
|
|
4370
|
-
return result;
|
|
4371
|
-
}
|
|
4372
|
-
|
|
4373
|
-
/**
|
|
4374
|
-
* Minify SVG path data by reducing numeric precision and removing unnecessary spaces
|
|
4375
|
-
* @param {string} pathData - SVG path data string
|
|
4376
|
-
* @param {number} precision - Decimal precision for coordinates
|
|
4377
|
-
* @returns {string} Minified path data
|
|
4378
|
-
*/
|
|
4379
|
-
function minifyPathData(pathData, precision = 3) {
|
|
4380
|
-
if (!pathData || typeof pathData !== 'string') return pathData;
|
|
4381
|
-
|
|
4382
|
-
// First, minify all numbers
|
|
4383
|
-
let result = pathData.replace(RE_NUMERIC_VALUE, (match) => {
|
|
4384
|
-
return minifyNumber(match, precision);
|
|
4385
|
-
});
|
|
4386
|
-
|
|
4387
|
-
// Remove unnecessary spaces around path commands
|
|
4388
|
-
// Safe to remove space after a command letter when it’s followed by a number
|
|
4389
|
-
// (which may be negative or start with a decimal point)
|
|
4390
|
-
// `M 10 20` → `M10 20`, `L -5 -3` → `L-5-3`, `M .5 .3` → `M.5.3`
|
|
4391
|
-
result = result.replace(/([MLHVCSQTAZmlhvcsqtaz])\s+(?=-?\.?\d)/g, '$1');
|
|
4392
|
-
|
|
4393
|
-
// Safe to remove space before command letter when preceded by a number
|
|
4394
|
-
// `0 L` → `0L`, `20 M` → `20M`, `.5 L` → `.5L`
|
|
4395
|
-
result = result.replace(/([\d.])\s+([MLHVCSQTAZmlhvcsqtaz])/g, '$1$2');
|
|
4396
|
-
|
|
4397
|
-
// Safe to remove space before negative number when preceded by a number
|
|
4398
|
-
// `10 -20` → `10-20`, `.5 -.3` → `.5-.3` (minus sign is always a separator)
|
|
4399
|
-
result = result.replace(/([\d.])\s+(-)/g, '$1$2');
|
|
4400
|
-
|
|
4401
|
-
// Safe to remove space between two decimal numbers (decimal point acts as separator)
|
|
4402
|
-
// `.5 .3` → `.5.3` (only when previous char is `.`, indicating a complete decimal)
|
|
4403
|
-
// Note: `0 .3` must not become `0.3` (that would change two numbers into one)
|
|
4404
|
-
result = result.replace(/(\.\d*)\s+(\.)/g, '$1$2');
|
|
4405
|
-
|
|
4406
|
-
return result;
|
|
4407
|
-
}
|
|
4408
|
-
|
|
4409
|
-
/**
|
|
4410
|
-
* Minify whitespace in numeric attribute values
|
|
4411
|
-
* Examples:
|
|
4412
|
-
* - “10 , 20" → "10,20"
|
|
4413
|
-
* - "translate( 10 20 )" → "translate(10 20)"
|
|
4414
|
-
* - "100, 10 40, 198" → "100,10 40,198"
|
|
4415
|
-
*
|
|
4416
|
-
* @param {string} value - Attribute value to minify
|
|
4417
|
-
* @returns {string} Minified value
|
|
4418
|
-
*/
|
|
4419
|
-
function minifyAttributeWhitespace(value) {
|
|
4420
|
-
if (!value || typeof value !== 'string') return value;
|
|
4421
|
-
|
|
4422
|
-
return value
|
|
4423
|
-
// Remove spaces around commas
|
|
4424
|
-
.replace(/\s*,\s*/g, ',')
|
|
4425
|
-
// Remove spaces around parentheses
|
|
4426
|
-
.replace(/\(\s+/g, '(')
|
|
4427
|
-
.replace(/\s+\)/g, ')')
|
|
4428
|
-
// Collapse multiple spaces to single space
|
|
4429
|
-
.replace(/\s+/g, ' ')
|
|
4430
|
-
// Trim leading/trailing whitespace
|
|
4431
|
-
.trim();
|
|
4432
|
-
}
|
|
4433
|
-
|
|
4434
|
-
/**
|
|
4435
|
-
* Minify color values (hex shortening, rgb to hex conversion, named colors)
|
|
4436
|
-
* Only processes simple color values; preserves case-sensitive references like `url(#id)`
|
|
4437
|
-
* @param {string} color - Color value to minify
|
|
4438
|
-
* @returns {string} Minified color value
|
|
4439
|
-
*/
|
|
4440
|
-
function minifyColor(color) {
|
|
4441
|
-
if (!color || typeof color !== 'string') return color;
|
|
4442
|
-
|
|
4443
|
-
const trimmed = color.trim();
|
|
4444
|
-
|
|
4445
|
-
// Don’t process values that aren’t simple colors (preserve case-sensitive references)
|
|
4446
|
-
// `url(#id)`, `var(--name)`, `inherit`, `currentColor`, etc.
|
|
4447
|
-
if (trimmed.includes('url(') || trimmed.includes('var(') || trimmed === 'inherit' || trimmed === 'currentColor') {
|
|
4448
|
-
return trimmed;
|
|
4449
|
-
}
|
|
4450
|
-
|
|
4451
|
-
// Now safe to lowercase for color matching
|
|
4452
|
-
const lower = trimmed.toLowerCase();
|
|
4453
|
-
|
|
4454
|
-
// Shorten 6-digit hex to 3-digit when possible
|
|
4455
|
-
// `#aabbcc` → `#abc`, `#000000` → `#000`
|
|
4456
|
-
const hexMatch = lower.match(/^#([0-9a-f]{6})$/);
|
|
4457
|
-
if (hexMatch) {
|
|
4458
|
-
const hex = hexMatch[1];
|
|
4459
|
-
if (hex[0] === hex[1] && hex[2] === hex[3] && hex[4] === hex[5]) {
|
|
4460
|
-
const shortened = '#' + hex[0] + hex[2] + hex[4];
|
|
4461
|
-
// Try to use named color if shorter
|
|
4462
|
-
return NAMED_COLORS[shortened] || shortened;
|
|
4463
|
-
}
|
|
4464
|
-
// Can’t shorten, but check for named color
|
|
4465
|
-
return NAMED_COLORS[lower] || lower;
|
|
4466
|
-
}
|
|
4467
|
-
|
|
4468
|
-
// Match 3-digit hex colors
|
|
4469
|
-
const hex3Match = lower.match(/^#[0-9a-f]{3}$/);
|
|
4470
|
-
if (hex3Match) {
|
|
4471
|
-
// Check if there’s a shorter named color
|
|
4472
|
-
return NAMED_COLORS[lower] || lower;
|
|
4473
|
-
}
|
|
4474
|
-
|
|
4475
|
-
// Convert rgb() to hex
|
|
4476
|
-
const rgbMatch = lower.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
|
|
4477
|
-
if (rgbMatch) {
|
|
4478
|
-
const r = parseInt(rgbMatch[1], 10);
|
|
4479
|
-
const g = parseInt(rgbMatch[2], 10);
|
|
4480
|
-
const b = parseInt(rgbMatch[3], 10);
|
|
4481
|
-
|
|
4482
|
-
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
|
|
4483
|
-
const toHex = (n) => {
|
|
4484
|
-
const h = n.toString(16);
|
|
4485
|
-
return h.length === 1 ? '0' + h : h;
|
|
4486
|
-
};
|
|
4487
|
-
const hexColor = '#' + toHex(r) + toHex(g) + toHex(b);
|
|
4488
|
-
|
|
4489
|
-
// Try to shorten if possible
|
|
4490
|
-
if (hexColor[1] === hexColor[2] && hexColor[3] === hexColor[4] && hexColor[5] === hexColor[6]) {
|
|
4491
|
-
const shortened = '#' + hexColor[1] + hexColor[3] + hexColor[5];
|
|
4492
|
-
return NAMED_COLORS[shortened] || shortened;
|
|
4493
|
-
}
|
|
4494
|
-
return NAMED_COLORS[hexColor] || hexColor;
|
|
4495
|
-
}
|
|
4496
|
-
}
|
|
4497
|
-
|
|
4498
|
-
// Not a recognized color format, return as-is (preserves case)
|
|
4499
|
-
return trimmed;
|
|
4500
|
-
}
|
|
4501
|
-
|
|
4502
|
-
// Attributes that contain numeric sequences or path data
|
|
4503
|
-
const NUMERIC_ATTRS = new Set([
|
|
4504
|
-
'd', // Path data
|
|
4505
|
-
'points', // Polygon/polyline points
|
|
4506
|
-
'viewBox', // `viewBox` coordinates
|
|
4507
|
-
'transform', // Transform functions
|
|
4508
|
-
'x', 'y', 'x1', 'y1', 'x2', 'y2', // Coordinates
|
|
4509
|
-
'cx', 'cy', 'r', 'rx', 'ry', // Circle/ellipse
|
|
4510
|
-
'width', 'height', // Dimensions
|
|
4511
|
-
'dx', 'dy', // Text offsets
|
|
4512
|
-
'offset', // Gradient offset
|
|
4513
|
-
'startOffset', // `textPath`
|
|
4514
|
-
'pathLength', // Path length
|
|
4515
|
-
'stdDeviation', // Filter params
|
|
4516
|
-
'baseFrequency', // Turbulence
|
|
4517
|
-
'k1', 'k2', 'k3', 'k4' // Composite filter
|
|
4518
|
-
]);
|
|
4519
|
-
|
|
4520
|
-
// Attributes that contain color values
|
|
4521
|
-
const COLOR_ATTRS = new Set([
|
|
4522
|
-
'fill',
|
|
4523
|
-
'stroke',
|
|
4524
|
-
'stop-color',
|
|
4525
|
-
'flood-color',
|
|
4526
|
-
'lighting-color'
|
|
4527
|
-
]);
|
|
4528
|
-
|
|
4529
|
-
// Pre-compiled regexes for identity transform detection (compiled once at module load)
|
|
4530
|
-
// Separator pattern: Accepts comma with optional spaces or one or more spaces
|
|
4531
|
-
const SEP = '(?:\\s*,\\s*|\\s+)';
|
|
4532
|
-
|
|
4533
|
-
// `translate(0)`, `translate(0,0)`, `translate(0 0)` (matches 0, 0.0, 0.00, etc.)
|
|
4534
|
-
const IDENTITY_TRANSLATE_RE = new RegExp(`^translate\\s*\\(\\s*0(?:\\.0+)?\\s*(?:${SEP}0(?:\\.0+)?\\s*)?\\)$`, 'i');
|
|
4535
|
-
|
|
4536
|
-
// `scale(1)`, `scale(1,1)`, `scale(1 1)` (matches 1, 1.0, 1.00, etc.)
|
|
4537
|
-
const IDENTITY_SCALE_RE = new RegExp(`^scale\\s*\\(\\s*1(?:\\.0+)?\\s*(?:${SEP}1(?:\\.0+)?\\s*)?\\)$`, 'i');
|
|
4538
|
-
|
|
4539
|
-
// `rotate(0)`, `rotate(0 cx cy)`, `rotate(0, cx, cy)` (matches 0, 0.0, 0.00, etc.)
|
|
4540
|
-
// Note: `cx` and `cy` must be valid numbers if present
|
|
4541
|
-
const IDENTITY_ROTATE_RE = new RegExp(`^rotate\\s*\\(\\s*0(?:\\.0+)?\\s*(?:${SEP}-?\\d+(?:\\.\\d+)?${SEP}-?\\d+(?:\\.\\d+)?)?\\s*\\)$`, 'i');
|
|
4542
|
-
|
|
4543
|
-
// `skewX(0)`, `skewY(0)` (matches 0, 0.0, 0.00, etc.)
|
|
4544
|
-
const IDENTITY_SKEW_RE = /^skew[XY]\s*\(\s*0(?:\.0+)?\s*\)$/i;
|
|
4545
|
-
|
|
4546
|
-
// `matrix(1,0,0,1,0,0)`, `matrix(1 0 0 1 0 0)`—identity matrix (matches 1.0/0.0 variants)
|
|
4547
|
-
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');
|
|
4548
|
-
|
|
4549
|
-
/**
|
|
4550
|
-
* Check if a transform attribute has no effect (identity transform)
|
|
4551
|
-
* @param {string} transform - Transform attribute value
|
|
4552
|
-
* @returns {boolean} True if transform is an identity (has no effect)
|
|
4553
|
-
*/
|
|
4554
|
-
function isIdentityTransform(transform) {
|
|
4555
|
-
if (!transform || typeof transform !== 'string') return false;
|
|
4556
|
-
|
|
4557
|
-
const trimmed = transform.trim();
|
|
4558
|
-
|
|
4559
|
-
// Check for common identity transforms using pre-compiled regexes
|
|
4560
|
-
return IDENTITY_TRANSLATE_RE.test(trimmed) ||
|
|
4561
|
-
IDENTITY_SCALE_RE.test(trimmed) ||
|
|
4562
|
-
IDENTITY_ROTATE_RE.test(trimmed) ||
|
|
4563
|
-
IDENTITY_SKEW_RE.test(trimmed) ||
|
|
4564
|
-
IDENTITY_MATRIX_RE.test(trimmed);
|
|
4565
|
-
}
|
|
4566
|
-
|
|
4567
|
-
/**
|
|
4568
|
-
* Check if an attribute should be removed based on default value
|
|
4569
|
-
* @param {string} tag - Element tag name (e.g., `svg`, `rect`, `path`)
|
|
4570
|
-
* @param {string} name - Attribute name
|
|
4571
|
-
* @param {string} value - Attribute value
|
|
4572
|
-
* @returns {boolean} True if attribute can be removed
|
|
4573
|
-
*/
|
|
4574
|
-
function isDefaultAttribute(tag, name, value) {
|
|
4575
|
-
// Special case: `overflow="visible"` is unsafe for root `<svg>` element
|
|
4576
|
-
// Root SVG may need explicit `overflow="visible"` to show clipped content
|
|
4577
|
-
if (name === 'overflow' && value === 'visible') {
|
|
4578
|
-
return tag !== 'svg'; // Only remove for non-root SVG elements
|
|
4579
|
-
}
|
|
4580
|
-
|
|
4581
|
-
const checker = SVG_DEFAULT_ATTRS[name];
|
|
4582
|
-
if (!checker) return false;
|
|
4583
|
-
|
|
4584
|
-
// Special case: Don’t remove `fill="black"` if stroke exists without fill
|
|
4585
|
-
// This would change the rendering (stroke-only shapes would gain black fill)
|
|
4586
|
-
if (name === 'fill' && checker(value)) {
|
|
4587
|
-
// This check would require looking at other attributes on the same element
|
|
4588
|
-
// For safety, we’ll keep this conservative and not remove `fill="black"`
|
|
4589
|
-
// in the initial implementation. Can be refined later.
|
|
4590
|
-
return false;
|
|
4591
|
-
}
|
|
4592
|
-
|
|
4593
|
-
return checker(value);
|
|
4594
|
-
}
|
|
4595
|
-
|
|
4596
|
-
/**
|
|
4597
|
-
* Minify SVG attribute value based on attribute name
|
|
4598
|
-
* @param {string} name - Attribute name
|
|
4599
|
-
* @param {string} value - Attribute value
|
|
4600
|
-
* @param {Object} options - Minification options
|
|
4601
|
-
* @returns {string} Minified attribute value
|
|
4602
|
-
*/
|
|
4603
|
-
function minifySVGAttributeValue(name, value, options = {}) {
|
|
4604
|
-
if (!value || typeof value !== 'string') return value;
|
|
4605
|
-
|
|
4606
|
-
const { precision = 3, minifyColors = true } = options;
|
|
4607
|
-
|
|
4608
|
-
// Path data gets special treatment
|
|
4609
|
-
if (name === 'd') {
|
|
4610
|
-
return minifyPathData(value, precision);
|
|
4611
|
-
}
|
|
4612
|
-
|
|
4613
|
-
// Numeric attributes get precision reduction and whitespace minification
|
|
4614
|
-
if (NUMERIC_ATTRS.has(name)) {
|
|
4615
|
-
const minified = value.replace(RE_NUMERIC_VALUE, (match) => {
|
|
4616
|
-
return minifyNumber(match, precision);
|
|
4617
|
-
});
|
|
4618
|
-
return minifyAttributeWhitespace(minified);
|
|
4619
|
-
}
|
|
4620
|
-
|
|
4621
|
-
// Color attributes get color minification
|
|
4622
|
-
if (minifyColors && COLOR_ATTRS.has(name)) {
|
|
4623
|
-
return minifyColor(value);
|
|
4624
|
-
}
|
|
4625
|
-
|
|
4626
|
-
return value;
|
|
4627
|
-
}
|
|
4628
|
-
|
|
4629
|
-
/**
|
|
4630
|
-
* Check if an SVG attribute can be removed
|
|
4631
|
-
* @param {string} tag - Element tag name (e.g., `svg`, `rect`, `path`)
|
|
4632
|
-
* @param {string} name - Attribute name
|
|
4633
|
-
* @param {string} value - Attribute value
|
|
4634
|
-
* @param {Object} options - Minification options
|
|
4635
|
-
* @returns {boolean} True if attribute should be removed
|
|
4636
|
-
*/
|
|
4637
|
-
function shouldRemoveSVGAttribute(tag, name, value, options = {}) {
|
|
4638
|
-
const { removeDefaults = true } = options;
|
|
4639
|
-
|
|
4640
|
-
if (!removeDefaults) return false;
|
|
4641
|
-
|
|
4642
|
-
// Check for identity transforms
|
|
4643
|
-
if (name === 'transform' && isIdentityTransform(value)) {
|
|
4644
|
-
return true;
|
|
4645
|
-
}
|
|
4646
|
-
|
|
4647
|
-
return isDefaultAttribute(tag, name, value);
|
|
4648
|
-
}
|
|
4649
|
-
|
|
4650
|
-
/**
|
|
4651
|
-
* Get default SVG minification options
|
|
4652
|
-
* @param {Object} userOptions - User-provided options
|
|
4653
|
-
* @returns {Object} Complete options object with defaults
|
|
4654
|
-
*/
|
|
4655
|
-
function getSVGMinifierOptions(userOptions) {
|
|
4656
|
-
if (typeof userOptions === 'boolean') {
|
|
4657
|
-
return userOptions ? {
|
|
4658
|
-
precision: 3,
|
|
4659
|
-
removeDefaults: true,
|
|
4660
|
-
minifyColors: true
|
|
4661
|
-
} : null;
|
|
4662
|
-
}
|
|
4663
|
-
|
|
4664
|
-
if (typeof userOptions === 'object' && userOptions !== null) {
|
|
4665
|
-
return {
|
|
4666
|
-
precision: userOptions.precision ?? 3,
|
|
4667
|
-
removeDefaults: userOptions.removeDefaults ?? true,
|
|
4668
|
-
minifyColors: userOptions.minifyColors ?? true
|
|
4669
|
-
};
|
|
4670
|
-
}
|
|
4671
|
-
|
|
4672
|
-
return null;
|
|
4673
|
-
}
|
|
4674
|
-
|
|
4675
4253
|
// Single source of truth for minifier option names, descriptions, types, and shared defaults
|
|
4676
4254
|
|
|
4677
4255
|
|
|
@@ -4713,11 +4291,13 @@ function shouldMinifyInnerHTML(options) {
|
|
|
4713
4291
|
* @param {Function} deps.getLightningCSS - Function to lazily load Lightning CSS
|
|
4714
4292
|
* @param {Function} deps.getTerser - Function to lazily load Terser
|
|
4715
4293
|
* @param {Function} deps.getSwc - Function to lazily load @swc/core
|
|
4294
|
+
* @param {Function} deps.getSvgo - Function to lazily load SVGO
|
|
4716
4295
|
* @param {LRU} deps.cssMinifyCache - CSS minification cache
|
|
4717
4296
|
* @param {LRU} deps.jsMinifyCache - JS minification cache
|
|
4297
|
+
* @param {LRU} deps.svgMinifyCache - SVG minification cache
|
|
4718
4298
|
* @returns {MinifierOptions} Normalized options with defaults applied
|
|
4719
4299
|
*/
|
|
4720
|
-
const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssMinifyCache, jsMinifyCache } = {}) => {
|
|
4300
|
+
const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getSvgo, cssMinifyCache, jsMinifyCache, svgMinifyCache } = {}) => {
|
|
4721
4301
|
const options = {
|
|
4722
4302
|
name: lowercase,
|
|
4723
4303
|
canCollapseWhitespace,
|
|
@@ -5018,11 +4598,54 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
5018
4598
|
return text;
|
|
5019
4599
|
}
|
|
5020
4600
|
};
|
|
5021
|
-
} else if (key === 'minifySVG') {
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
4601
|
+
} else if (key === 'minifySVG' && typeof option !== 'function') {
|
|
4602
|
+
if (!option) {
|
|
4603
|
+
return;
|
|
4604
|
+
}
|
|
4605
|
+
|
|
4606
|
+
const svgoOptions = typeof option === 'object' ? option : {};
|
|
4607
|
+
|
|
4608
|
+
// Pre-compute option signature for cache keys
|
|
4609
|
+
const svgSig = stableStringify({
|
|
4610
|
+
...svgoOptions,
|
|
4611
|
+
cont: !!options.continueOnMinifyError
|
|
4612
|
+
});
|
|
4613
|
+
|
|
4614
|
+
options.minifySVG = async function (svgContent) {
|
|
4615
|
+
if (!svgContent || !svgContent.trim()) {
|
|
4616
|
+
return svgContent;
|
|
4617
|
+
}
|
|
4618
|
+
|
|
4619
|
+
// Cache key
|
|
4620
|
+
const svgKey = svgContent.length > 2048
|
|
4621
|
+
? (svgContent.length + '|' + svgContent.slice(0, 50) + svgContent.slice(-50) + '|' + svgSig)
|
|
4622
|
+
: (svgContent + '|' + svgSig);
|
|
4623
|
+
|
|
4624
|
+
try {
|
|
4625
|
+
const cached = svgMinifyCache.get(svgKey);
|
|
4626
|
+
if (cached) {
|
|
4627
|
+
return await cached;
|
|
4628
|
+
}
|
|
4629
|
+
|
|
4630
|
+
const inFlight = (async () => {
|
|
4631
|
+
const optimize = await getSvgo();
|
|
4632
|
+
const result = optimize(svgContent, svgoOptions);
|
|
4633
|
+
return result.data;
|
|
4634
|
+
})();
|
|
4635
|
+
|
|
4636
|
+
svgMinifyCache.set(svgKey, inFlight);
|
|
4637
|
+
const resolved = await inFlight;
|
|
4638
|
+
svgMinifyCache.set(svgKey, resolved);
|
|
4639
|
+
return resolved;
|
|
4640
|
+
} catch (err) {
|
|
4641
|
+
svgMinifyCache.delete(svgKey);
|
|
4642
|
+
if (!options.continueOnMinifyError) {
|
|
4643
|
+
throw err;
|
|
4644
|
+
}
|
|
4645
|
+
options.log && options.log(err);
|
|
4646
|
+
return svgContent;
|
|
4647
|
+
}
|
|
4648
|
+
};
|
|
5026
4649
|
} else if (key === 'customAttrCollapse') {
|
|
5027
4650
|
// Single regex pattern
|
|
5028
4651
|
options[key] = parseRegExp(option);
|
|
@@ -5426,17 +5049,6 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
5426
5049
|
return attrValue;
|
|
5427
5050
|
}
|
|
5428
5051
|
return minifyHTMLSelf(attrValue, options, true);
|
|
5429
|
-
} else if (options.insideSVG && options.minifySVG) {
|
|
5430
|
-
// Apply SVG-specific attribute minification when inside SVG elements
|
|
5431
|
-
try {
|
|
5432
|
-
return minifySVGAttributeValue(attrName, attrValue, options.minifySVG);
|
|
5433
|
-
} catch (err) {
|
|
5434
|
-
if (!options.continueOnMinifyError) {
|
|
5435
|
-
throw err;
|
|
5436
|
-
}
|
|
5437
|
-
options.log && options.log(err);
|
|
5438
|
-
return attrValue;
|
|
5439
|
-
}
|
|
5440
5052
|
}
|
|
5441
5053
|
return attrValue;
|
|
5442
5054
|
}
|
|
@@ -5477,9 +5089,7 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
5477
5089
|
(options.removeScriptTypeAttributes && tag === 'script' &&
|
|
5478
5090
|
attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue)) ||
|
|
5479
5091
|
(options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
|
|
5480
|
-
attrName === 'type' && isStyleLinkTypeAttribute(attrValue))
|
|
5481
|
-
(options.insideSVG && options.minifySVG &&
|
|
5482
|
-
shouldRemoveSVGAttribute(tag, attrName, attrValue, options.minifySVG))) {
|
|
5092
|
+
attrName === 'type' && isStyleLinkTypeAttribute(attrValue))) {
|
|
5483
5093
|
return;
|
|
5484
5094
|
}
|
|
5485
5095
|
|
|
@@ -5886,9 +5496,18 @@ async function getSwc() {
|
|
|
5886
5496
|
return swcPromise;
|
|
5887
5497
|
}
|
|
5888
5498
|
|
|
5499
|
+
let svgoPromise;
|
|
5500
|
+
async function getSvgo() {
|
|
5501
|
+
if (!svgoPromise) {
|
|
5502
|
+
svgoPromise = import('svgo').then(m => m.optimize);
|
|
5503
|
+
}
|
|
5504
|
+
return svgoPromise;
|
|
5505
|
+
}
|
|
5506
|
+
|
|
5889
5507
|
// Minification caches (initialized on first use with configurable sizes)
|
|
5890
5508
|
let cssMinifyCache = null;
|
|
5891
5509
|
let jsMinifyCache = null;
|
|
5510
|
+
let svgMinifyCache = null;
|
|
5892
5511
|
|
|
5893
5512
|
// Pre-compiled patterns for script merging (avoid repeated allocation in hot path)
|
|
5894
5513
|
const RE_SCRIPT_ATTRS = /([^\s=]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
@@ -6058,6 +5677,15 @@ function mergeConsecutiveScripts(html) {
|
|
|
6058
5677
|
*
|
|
6059
5678
|
* Default: `500`
|
|
6060
5679
|
*
|
|
5680
|
+
* @prop {number} [cacheSVG]
|
|
5681
|
+
* The maximum number of entries for the SVG minification cache. Higher
|
|
5682
|
+
* values improve performance for inputs with repeated SVG content.
|
|
5683
|
+
* - Cache is created on first `minify()` call and persists for the process lifetime
|
|
5684
|
+
* - Cache size is locked after first call—subsequent calls reuse the same cache
|
|
5685
|
+
* - Explicit `0` values are coerced to `1` (minimum functional cache size)
|
|
5686
|
+
*
|
|
5687
|
+
* Default: `500`
|
|
5688
|
+
*
|
|
6061
5689
|
* @prop {boolean} [caseSensitive]
|
|
6062
5690
|
* When true, tag and attribute names are treated as case-sensitive.
|
|
6063
5691
|
* Useful for custom HTML tags.
|
|
@@ -6240,12 +5868,10 @@ function mergeConsecutiveScripts(html) {
|
|
|
6240
5868
|
*
|
|
6241
5869
|
* Default: `false`
|
|
6242
5870
|
*
|
|
6243
|
-
* @prop {boolean |
|
|
6244
|
-
* When true, enables SVG
|
|
6245
|
-
*
|
|
6246
|
-
*
|
|
6247
|
-
* - `removeDefaults`: Remove attributes with default values (e.g., `fill="black"`). Default: `true`
|
|
6248
|
-
* - `minifyColors`: Minify color values (hex shortening, rgb to hex conversion). Default: `true`
|
|
5871
|
+
* @prop {boolean | Object} [minifySVG]
|
|
5872
|
+
* When true, enables SVG minification using [SVGO](https://github.com/svg/svgo).
|
|
5873
|
+
* Complete SVG subtrees are extracted and optimized as a block.
|
|
5874
|
+
* If an object is provided, it is passed to SVGO as configuration options.
|
|
6249
5875
|
* If disabled, SVG content is minified using standard HTML rules only.
|
|
6250
5876
|
*
|
|
6251
5877
|
* Default: `false`
|
|
@@ -6829,6 +6455,11 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
6829
6455
|
trimTrailingWhitespace(charsIndex, nextTag);
|
|
6830
6456
|
}
|
|
6831
6457
|
|
|
6458
|
+
// SVG subtree capture: When SVGO is active, record buffer positions for post-processing
|
|
6459
|
+
const svgBlocks = []; // Array of { start, end } buffer indices
|
|
6460
|
+
let svgBufferStartIndex = -1;
|
|
6461
|
+
let svgDepth = 0;
|
|
6462
|
+
|
|
6832
6463
|
const parser = new HTMLParser(value, {
|
|
6833
6464
|
partialMarkup: partialMarkup ?? options.partialMarkup,
|
|
6834
6465
|
continueOnParseError: options.continueOnParseError,
|
|
@@ -6846,6 +6477,10 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
6846
6477
|
options.name = identity;
|
|
6847
6478
|
options.insideSVG = lowerTag === 'svg';
|
|
6848
6479
|
options.insideForeignContent = true;
|
|
6480
|
+
// Disable HTML-specific options that produce invalid XML
|
|
6481
|
+
options.removeAttributeQuotes = false;
|
|
6482
|
+
options.removeTagWhitespace = false;
|
|
6483
|
+
options.decodeEntities = false;
|
|
6849
6484
|
}
|
|
6850
6485
|
// `foreignObject` in SVG and `annotation-xml` in MathML contain HTML content
|
|
6851
6486
|
// Note: The element itself is in SVG/MathML namespace, only its children are HTML
|
|
@@ -6860,6 +6495,9 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
6860
6495
|
options.parentName = parentName; // Preserve for the element tag itself
|
|
6861
6496
|
options.name = options.htmlName || lowercase;
|
|
6862
6497
|
options.insideForeignContent = false;
|
|
6498
|
+
// Note: `removeAttributeQuotes`, `removeTagWhitespace`, and `decodeEntities`
|
|
6499
|
+
// stay disabled (inherited from SVG context) because the entire SVG block
|
|
6500
|
+
// must be valid XML for SVGO processing
|
|
6863
6501
|
useParentNameForTag = true;
|
|
6864
6502
|
}
|
|
6865
6503
|
tag = (useParentNameForTag ? options.parentName : options.name)(tag);
|
|
@@ -6911,6 +6549,14 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
6911
6549
|
}
|
|
6912
6550
|
}
|
|
6913
6551
|
|
|
6552
|
+
// Track SVG subtree for SVGO block processing
|
|
6553
|
+
if (lowerTag === 'svg' && options.minifySVG) {
|
|
6554
|
+
if (svgDepth === 0) {
|
|
6555
|
+
svgBufferStartIndex = buffer.length; // Record position before <svg> is pushed
|
|
6556
|
+
}
|
|
6557
|
+
svgDepth++;
|
|
6558
|
+
}
|
|
6559
|
+
|
|
6914
6560
|
const openTag = '<' + tag;
|
|
6915
6561
|
const hasUnarySlash = unarySlash && options.keepClosingSlash;
|
|
6916
6562
|
|
|
@@ -7041,6 +6687,15 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
7041
6687
|
currentChars += '|';
|
|
7042
6688
|
}
|
|
7043
6689
|
}
|
|
6690
|
+
|
|
6691
|
+
// SVG subtree capture: Record end position for post-processing with SVGO
|
|
6692
|
+
if (lowerTag === 'svg' && options.minifySVG && svgDepth > 0) {
|
|
6693
|
+
svgDepth--;
|
|
6694
|
+
if (svgDepth === 0 && svgBufferStartIndex >= 0) {
|
|
6695
|
+
svgBlocks.push({ start: svgBufferStartIndex, end: buffer.length });
|
|
6696
|
+
svgBufferStartIndex = -1;
|
|
6697
|
+
}
|
|
6698
|
+
}
|
|
7044
6699
|
},
|
|
7045
6700
|
chars: async function (text, prevTag, nextTag, prevAttrs, nextAttrs) {
|
|
7046
6701
|
prevTag = prevTag === '' ? 'comment' : prevTag;
|
|
@@ -7265,6 +6920,19 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
7265
6920
|
|
|
7266
6921
|
await parser.parse();
|
|
7267
6922
|
|
|
6923
|
+
// Post-processing: Optimize SVG blocks with SVGO
|
|
6924
|
+
// Run all SVGO calls in parallel, then splice results in reverse to preserve indices
|
|
6925
|
+
if (options.minifySVG && svgBlocks.length) {
|
|
6926
|
+
const optimized = await Promise.all(
|
|
6927
|
+
svgBlocks.map(({ start, end }) =>
|
|
6928
|
+
options.minifySVG(buffer.slice(start, end).join(''))
|
|
6929
|
+
)
|
|
6930
|
+
);
|
|
6931
|
+
for (let i = svgBlocks.length - 1; i >= 0; i--) {
|
|
6932
|
+
buffer.splice(svgBlocks[i].start, svgBlocks[i].end - svgBlocks[i].start, optimized[i]);
|
|
6933
|
+
}
|
|
6934
|
+
}
|
|
6935
|
+
|
|
7268
6936
|
if (options.removeOptionalTags) {
|
|
7269
6937
|
// `<html>` may be omitted if first thing inside is not a comment
|
|
7270
6938
|
// `<head>` or `<body>` may be omitted if empty
|
|
@@ -7351,7 +7019,7 @@ function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
|
|
|
7351
7019
|
* Important behavior notes:
|
|
7352
7020
|
* - Caches are created on the first `minify()` call and persist for the lifetime of the process
|
|
7353
7021
|
* - Cache sizes are locked after first initialization—subsequent calls use the same caches
|
|
7354
|
-
* even if different `cacheCSS`/`cacheJS` options are provided
|
|
7022
|
+
* even if different `cacheCSS`/`cacheJS`/`cacheSVG` options are provided
|
|
7355
7023
|
* - The first call’s options determine the cache sizes for subsequent calls
|
|
7356
7024
|
* - Explicit `0` values are coerced to `1` (minimum functional cache size)
|
|
7357
7025
|
*/
|
|
@@ -7375,16 +7043,20 @@ function initCaches(options) {
|
|
|
7375
7043
|
: (parseEnvCacheSize(process.env.HMN_CACHE_CSS) ?? defaultSize);
|
|
7376
7044
|
const jsSize = options.cacheJS !== undefined ? options.cacheJS
|
|
7377
7045
|
: (parseEnvCacheSize(process.env.HMN_CACHE_JS) ?? defaultSize);
|
|
7046
|
+
const svgSize = options.cacheSVG !== undefined ? options.cacheSVG
|
|
7047
|
+
: (parseEnvCacheSize(process.env.HMN_CACHE_SVG) ?? defaultSize);
|
|
7378
7048
|
|
|
7379
7049
|
// Coerce `0` to `1` (minimum functional cache size) to avoid immediate eviction
|
|
7380
7050
|
const cssFinalSize = cssSize === 0 ? 1 : cssSize;
|
|
7381
7051
|
const jsFinalSize = jsSize === 0 ? 1 : jsSize;
|
|
7052
|
+
const svgFinalSize = svgSize === 0 ? 1 : svgSize;
|
|
7382
7053
|
|
|
7383
7054
|
cssMinifyCache = new LRU(cssFinalSize);
|
|
7384
7055
|
jsMinifyCache = new LRU(jsFinalSize);
|
|
7056
|
+
svgMinifyCache = new LRU(svgFinalSize);
|
|
7385
7057
|
}
|
|
7386
7058
|
|
|
7387
|
-
return { cssMinifyCache, jsMinifyCache };
|
|
7059
|
+
return { cssMinifyCache, jsMinifyCache, svgMinifyCache };
|
|
7388
7060
|
}
|
|
7389
7061
|
|
|
7390
7062
|
/**
|
|
@@ -7402,6 +7074,7 @@ const minify$1 = async function (value, options) {
|
|
|
7402
7074
|
getLightningCSS,
|
|
7403
7075
|
getTerser,
|
|
7404
7076
|
getSwc,
|
|
7077
|
+
getSvgo,
|
|
7405
7078
|
...caches
|
|
7406
7079
|
});
|
|
7407
7080
|
let result = await minifyHTML(value, options);
|