html-minifier-next 4.14.1 → 4.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -31
- package/cli.js +3 -2
- package/dist/htmlminifier.cjs +323 -4
- package/dist/htmlminifier.esm.bundle.js +323 -4
- package/dist/types/htmlminifier.d.ts +15 -0
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/lib/attributes.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts.map +1 -1
- package/dist/types/lib/svg.d.ts +23 -0
- package/dist/types/lib/svg.d.ts.map +1 -0
- package/dist/types/lib/whitespace.d.ts.map +1 -1
- package/dist/types/presets.d.ts +1 -0
- package/dist/types/presets.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/htmlminifier.js +11 -0
- package/src/htmlparser.js +1 -1
- package/src/lib/attributes.js +15 -1
- package/src/lib/options.js +10 -2
- package/src/lib/svg.js +272 -0
- package/src/lib/whitespace.js +16 -1
- package/src/presets.js +1 -0
|
@@ -2804,7 +2804,7 @@ class HTMLParser {
|
|
|
2804
2804
|
}
|
|
2805
2805
|
}
|
|
2806
2806
|
|
|
2807
|
-
// https://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
|
|
2807
|
+
// https://web.archive.org/web/20241201212701/https://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
|
|
2808
2808
|
if (/^<!\[/.test(html)) {
|
|
2809
2809
|
const conditionalEnd = html.indexOf(']>');
|
|
2810
2810
|
|
|
@@ -3373,6 +3373,7 @@ const presets = {
|
|
|
3373
3373
|
decodeEntities: true,
|
|
3374
3374
|
minifyCSS: true,
|
|
3375
3375
|
minifyJS: true,
|
|
3376
|
+
minifySVG: true,
|
|
3376
3377
|
minifyURLs: true,
|
|
3377
3378
|
noNewlinesBeforeTagClose: true,
|
|
3378
3379
|
processConditionalComments: true,
|
|
@@ -3747,10 +3748,24 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements,
|
|
|
3747
3748
|
if (trimLeft && !options.collapseInlineTagWhitespace) {
|
|
3748
3749
|
trimLeft = prevTag.charAt(0) === '/' ? !inlineElements.has(prevTag.slice(1)) : !inlineTextSet.has(prevTag);
|
|
3749
3750
|
}
|
|
3751
|
+
// When `collapseInlineTagWhitespace` is enabled, still preserve whitespace around inline text elements
|
|
3752
|
+
if (trimLeft && options.collapseInlineTagWhitespace) {
|
|
3753
|
+
const tagName = prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag;
|
|
3754
|
+
if (inlineElementsToKeepWhitespaceWithin.has(tagName)) {
|
|
3755
|
+
trimLeft = false;
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3750
3758
|
let trimRight = nextTag && !inlineElementsToKeepWhitespace.has(nextTag);
|
|
3751
3759
|
if (trimRight && !options.collapseInlineTagWhitespace) {
|
|
3752
3760
|
trimRight = nextTag.charAt(0) === '/' ? !inlineTextSet.has(nextTag.slice(1)) : !inlineElements.has(nextTag);
|
|
3753
3761
|
}
|
|
3762
|
+
// When `collapseInlineTagWhitespace` is enabled, still preserve whitespace around inline text elements
|
|
3763
|
+
if (trimRight && options.collapseInlineTagWhitespace) {
|
|
3764
|
+
const tagName = nextTag.charAt(0) === '/' ? nextTag.slice(1) : nextTag;
|
|
3765
|
+
if (inlineElementsToKeepWhitespaceWithin.has(tagName)) {
|
|
3766
|
+
trimRight = false;
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3754
3769
|
return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
|
|
3755
3770
|
}
|
|
3756
3771
|
|
|
@@ -6381,6 +6396,279 @@ async function processScript(text, options, currentAttrs, minifyHTML) {
|
|
|
6381
6396
|
return text;
|
|
6382
6397
|
}
|
|
6383
6398
|
|
|
6399
|
+
/**
|
|
6400
|
+
* Lightweight SVG optimizations:
|
|
6401
|
+
*
|
|
6402
|
+
* - Numeric precision reduction for coordinates and path data
|
|
6403
|
+
* - Whitespace removal in attribute values (numeric sequences)
|
|
6404
|
+
* - Default attribute removal (safe, well-documented defaults)
|
|
6405
|
+
* - Color minification (hex shortening, rgb() to hex)
|
|
6406
|
+
*/
|
|
6407
|
+
|
|
6408
|
+
/**
|
|
6409
|
+
* Default SVG attribute values that can be safely removed
|
|
6410
|
+
* Only includes well-documented, widely-supported defaults
|
|
6411
|
+
*/
|
|
6412
|
+
const SVG_DEFAULT_ATTRS = {
|
|
6413
|
+
// Fill and stroke defaults
|
|
6414
|
+
fill: value => value === 'black' || value === '#000' || value === '#000000',
|
|
6415
|
+
'fill-opacity': value => value === '1',
|
|
6416
|
+
'fill-rule': value => value === 'nonzero',
|
|
6417
|
+
stroke: value => value === 'none',
|
|
6418
|
+
'stroke-dasharray': value => value === 'none',
|
|
6419
|
+
'stroke-dashoffset': value => value === '0',
|
|
6420
|
+
'stroke-linecap': value => value === 'butt',
|
|
6421
|
+
'stroke-linejoin': value => value === 'miter',
|
|
6422
|
+
'stroke-miterlimit': value => value === '4',
|
|
6423
|
+
'stroke-opacity': value => value === '1',
|
|
6424
|
+
'stroke-width': value => value === '1',
|
|
6425
|
+
|
|
6426
|
+
// Text and font defaults
|
|
6427
|
+
'font-family': value => value === 'inherit',
|
|
6428
|
+
'font-size': value => value === 'medium',
|
|
6429
|
+
'font-style': value => value === 'normal',
|
|
6430
|
+
'font-variant': value => value === 'normal',
|
|
6431
|
+
'font-weight': value => value === 'normal',
|
|
6432
|
+
'letter-spacing': value => value === 'normal',
|
|
6433
|
+
'text-decoration': value => value === 'none',
|
|
6434
|
+
'text-anchor': value => value === 'start',
|
|
6435
|
+
|
|
6436
|
+
// Other common defaults
|
|
6437
|
+
opacity: value => value === '1',
|
|
6438
|
+
visibility: value => value === 'visible',
|
|
6439
|
+
display: value => value === 'inline',
|
|
6440
|
+
overflow: value => value === 'visible'
|
|
6441
|
+
};
|
|
6442
|
+
|
|
6443
|
+
/**
|
|
6444
|
+
* Minify numeric value by removing trailing zeros and unnecessary decimals
|
|
6445
|
+
* @param {string} num - Numeric string to minify
|
|
6446
|
+
* @param {number} precision - Maximum decimal places to keep
|
|
6447
|
+
* @returns {string} Minified numeric string
|
|
6448
|
+
*/
|
|
6449
|
+
function minifyNumber(num, precision = 3) {
|
|
6450
|
+
const parsed = parseFloat(num);
|
|
6451
|
+
|
|
6452
|
+
// Handle special cases
|
|
6453
|
+
if (isNaN(parsed)) return num;
|
|
6454
|
+
if (parsed === 0) return '0';
|
|
6455
|
+
if (!isFinite(parsed)) return num;
|
|
6456
|
+
|
|
6457
|
+
// Convert to fixed precision, then remove trailing zeros
|
|
6458
|
+
const fixed = parsed.toFixed(precision);
|
|
6459
|
+
const trimmed = fixed.replace(/\.?0+$/, '');
|
|
6460
|
+
|
|
6461
|
+
return trimmed || '0';
|
|
6462
|
+
}
|
|
6463
|
+
|
|
6464
|
+
/**
|
|
6465
|
+
* Minify SVG path data by reducing numeric precision
|
|
6466
|
+
* @param {string} pathData - SVG path data string
|
|
6467
|
+
* @param {number} precision - Decimal precision for coordinates
|
|
6468
|
+
* @returns {string} Minified path data
|
|
6469
|
+
*/
|
|
6470
|
+
function minifyPathData(pathData, precision = 3) {
|
|
6471
|
+
if (!pathData || typeof pathData !== 'string') return pathData;
|
|
6472
|
+
|
|
6473
|
+
// Match numbers (including scientific notation and negative values)
|
|
6474
|
+
// Regex: optional minus, digits, optional decimal point and more digits, optional exponent
|
|
6475
|
+
return pathData.replace(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g, (match) => {
|
|
6476
|
+
return minifyNumber(match, precision);
|
|
6477
|
+
});
|
|
6478
|
+
}
|
|
6479
|
+
|
|
6480
|
+
/**
|
|
6481
|
+
* Minify whitespace in numeric attribute values
|
|
6482
|
+
* Examples:
|
|
6483
|
+
* "10 , 20" → "10,20"
|
|
6484
|
+
* "translate( 10 20 )" → "translate(10 20)"
|
|
6485
|
+
* "100, 10 40, 198" → "100,10 40,198"
|
|
6486
|
+
*
|
|
6487
|
+
* @param {string} value - Attribute value to minify
|
|
6488
|
+
* @returns {string} Minified value
|
|
6489
|
+
*/
|
|
6490
|
+
function minifyAttributeWhitespace(value) {
|
|
6491
|
+
if (!value || typeof value !== 'string') return value;
|
|
6492
|
+
|
|
6493
|
+
return value
|
|
6494
|
+
// Remove spaces around commas
|
|
6495
|
+
.replace(/\s*,\s*/g, ',')
|
|
6496
|
+
// Remove spaces around parentheses
|
|
6497
|
+
.replace(/\(\s+/g, '(')
|
|
6498
|
+
.replace(/\s+\)/g, ')')
|
|
6499
|
+
// Collapse multiple spaces to single space
|
|
6500
|
+
.replace(/\s+/g, ' ')
|
|
6501
|
+
// Trim leading/trailing whitespace
|
|
6502
|
+
.trim();
|
|
6503
|
+
}
|
|
6504
|
+
|
|
6505
|
+
/**
|
|
6506
|
+
* Minify color values (hex shortening, rgb to hex conversion)
|
|
6507
|
+
* @param {string} color - Color value to minify
|
|
6508
|
+
* @returns {string} Minified color value
|
|
6509
|
+
*/
|
|
6510
|
+
function minifyColor(color) {
|
|
6511
|
+
if (!color || typeof color !== 'string') return color;
|
|
6512
|
+
|
|
6513
|
+
const trimmed = color.trim().toLowerCase();
|
|
6514
|
+
|
|
6515
|
+
// Shorten 6-digit hex to 3-digit when possible
|
|
6516
|
+
// #aabbcc → #abc, #000000 → #000
|
|
6517
|
+
const hexMatch = trimmed.match(/^#([0-9a-f]{6})$/);
|
|
6518
|
+
if (hexMatch) {
|
|
6519
|
+
const hex = hexMatch[1];
|
|
6520
|
+
if (hex[0] === hex[1] && hex[2] === hex[3] && hex[4] === hex[5]) {
|
|
6521
|
+
return '#' + hex[0] + hex[2] + hex[4];
|
|
6522
|
+
}
|
|
6523
|
+
}
|
|
6524
|
+
|
|
6525
|
+
// Convert rgb(255,255,255) to hex
|
|
6526
|
+
const rgbMatch = trimmed.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
|
|
6527
|
+
if (rgbMatch) {
|
|
6528
|
+
const r = parseInt(rgbMatch[1], 10);
|
|
6529
|
+
const g = parseInt(rgbMatch[2], 10);
|
|
6530
|
+
const b = parseInt(rgbMatch[3], 10);
|
|
6531
|
+
|
|
6532
|
+
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
|
|
6533
|
+
const toHex = (n) => {
|
|
6534
|
+
const h = n.toString(16);
|
|
6535
|
+
return h.length === 1 ? '0' + h : h;
|
|
6536
|
+
};
|
|
6537
|
+
const hexColor = '#' + toHex(r) + toHex(g) + toHex(b);
|
|
6538
|
+
|
|
6539
|
+
// Try to shorten if possible
|
|
6540
|
+
if (hexColor[1] === hexColor[2] && hexColor[3] === hexColor[4] && hexColor[5] === hexColor[6]) {
|
|
6541
|
+
return '#' + hexColor[1] + hexColor[3] + hexColor[5];
|
|
6542
|
+
}
|
|
6543
|
+
return hexColor;
|
|
6544
|
+
}
|
|
6545
|
+
}
|
|
6546
|
+
|
|
6547
|
+
return color;
|
|
6548
|
+
}
|
|
6549
|
+
|
|
6550
|
+
// Attributes that contain numeric sequences or path data
|
|
6551
|
+
const NUMERIC_ATTRS = new Set([
|
|
6552
|
+
'd', // Path data
|
|
6553
|
+
'points', // Polygon/polyline points
|
|
6554
|
+
'viewBox', // viewBox coordinates
|
|
6555
|
+
'transform', // Transform functions
|
|
6556
|
+
'x', 'y', 'x1', 'y1', 'x2', 'y2', // Coordinates
|
|
6557
|
+
'cx', 'cy', 'r', 'rx', 'ry', // Circle/ellipse
|
|
6558
|
+
'width', 'height', // Dimensions
|
|
6559
|
+
'dx', 'dy', // Text offsets
|
|
6560
|
+
'offset', // Gradient offset
|
|
6561
|
+
'startOffset', // textPath
|
|
6562
|
+
'pathLength', // Path length
|
|
6563
|
+
'stdDeviation', // Filter params
|
|
6564
|
+
'baseFrequency', // Turbulence
|
|
6565
|
+
'k1', 'k2', 'k3', 'k4' // Composite filter
|
|
6566
|
+
]);
|
|
6567
|
+
|
|
6568
|
+
// Attributes that contain color values
|
|
6569
|
+
const COLOR_ATTRS = new Set([
|
|
6570
|
+
'fill',
|
|
6571
|
+
'stroke',
|
|
6572
|
+
'stop-color',
|
|
6573
|
+
'flood-color',
|
|
6574
|
+
'lighting-color'
|
|
6575
|
+
]);
|
|
6576
|
+
|
|
6577
|
+
/**
|
|
6578
|
+
* Check if an attribute should be removed based on default value
|
|
6579
|
+
* @param {string} name - Attribute name
|
|
6580
|
+
* @param {string} value - Attribute value
|
|
6581
|
+
* @returns {boolean} True if attribute can be removed
|
|
6582
|
+
*/
|
|
6583
|
+
function isDefaultAttribute(name, value) {
|
|
6584
|
+
const checker = SVG_DEFAULT_ATTRS[name];
|
|
6585
|
+
if (!checker) return false;
|
|
6586
|
+
|
|
6587
|
+
// Special case: Don’t remove `fill="black"` if stroke exists without fill
|
|
6588
|
+
// This would change the rendering (stroke-only shapes would gain black fill)
|
|
6589
|
+
if (name === 'fill' && checker(value)) {
|
|
6590
|
+
// This check would require looking at other attributes on the same element
|
|
6591
|
+
// For safety, we’ll keep this conservative and not remove `fill="black"`
|
|
6592
|
+
// in the initial implementation. Can be refined later.
|
|
6593
|
+
return false;
|
|
6594
|
+
}
|
|
6595
|
+
|
|
6596
|
+
return checker(value);
|
|
6597
|
+
}
|
|
6598
|
+
|
|
6599
|
+
/**
|
|
6600
|
+
* Minify SVG attribute value based on attribute name
|
|
6601
|
+
* @param {string} name - Attribute name
|
|
6602
|
+
* @param {string} value - Attribute value
|
|
6603
|
+
* @param {Object} options - Minification options
|
|
6604
|
+
* @returns {string} Minified attribute value
|
|
6605
|
+
*/
|
|
6606
|
+
function minifySVGAttributeValue(name, value, options = {}) {
|
|
6607
|
+
if (!value || typeof value !== 'string') return value;
|
|
6608
|
+
|
|
6609
|
+
const { precision = 3, minifyColors = true } = options;
|
|
6610
|
+
|
|
6611
|
+
// Path data gets special treatment
|
|
6612
|
+
if (name === 'd') {
|
|
6613
|
+
return minifyPathData(value, precision);
|
|
6614
|
+
}
|
|
6615
|
+
|
|
6616
|
+
// Numeric attributes get precision reduction and whitespace minification
|
|
6617
|
+
if (NUMERIC_ATTRS.has(name)) {
|
|
6618
|
+
const minified = value.replace(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g, (match) => {
|
|
6619
|
+
return minifyNumber(match, precision);
|
|
6620
|
+
});
|
|
6621
|
+
return minifyAttributeWhitespace(minified);
|
|
6622
|
+
}
|
|
6623
|
+
|
|
6624
|
+
// Color attributes get color minification
|
|
6625
|
+
if (minifyColors && COLOR_ATTRS.has(name)) {
|
|
6626
|
+
return minifyColor(value);
|
|
6627
|
+
}
|
|
6628
|
+
|
|
6629
|
+
return value;
|
|
6630
|
+
}
|
|
6631
|
+
|
|
6632
|
+
/**
|
|
6633
|
+
* Check if an SVG attribute can be removed
|
|
6634
|
+
* @param {string} name - Attribute name
|
|
6635
|
+
* @param {string} value - Attribute value
|
|
6636
|
+
* @param {Object} options - Minification options
|
|
6637
|
+
* @returns {boolean} True if attribute should be removed
|
|
6638
|
+
*/
|
|
6639
|
+
function shouldRemoveSVGAttribute(name, value, options = {}) {
|
|
6640
|
+
const { removeDefaults = true } = options;
|
|
6641
|
+
|
|
6642
|
+
if (!removeDefaults) return false;
|
|
6643
|
+
|
|
6644
|
+
return isDefaultAttribute(name, value);
|
|
6645
|
+
}
|
|
6646
|
+
|
|
6647
|
+
/**
|
|
6648
|
+
* Get default SVG minification options
|
|
6649
|
+
* @param {Object} userOptions - User-provided options
|
|
6650
|
+
* @returns {Object} Complete options object with defaults
|
|
6651
|
+
*/
|
|
6652
|
+
function getSVGMinifierOptions(userOptions) {
|
|
6653
|
+
if (typeof userOptions === 'boolean') {
|
|
6654
|
+
return userOptions ? {
|
|
6655
|
+
precision: 3,
|
|
6656
|
+
removeDefaults: true,
|
|
6657
|
+
minifyColors: true
|
|
6658
|
+
} : null;
|
|
6659
|
+
}
|
|
6660
|
+
|
|
6661
|
+
if (typeof userOptions === 'object' && userOptions !== null) {
|
|
6662
|
+
return {
|
|
6663
|
+
precision: userOptions.precision ?? 3,
|
|
6664
|
+
removeDefaults: userOptions.removeDefaults ?? true,
|
|
6665
|
+
minifyColors: userOptions.minifyColors ?? true
|
|
6666
|
+
};
|
|
6667
|
+
}
|
|
6668
|
+
|
|
6669
|
+
return null;
|
|
6670
|
+
}
|
|
6671
|
+
|
|
6384
6672
|
// Imports
|
|
6385
6673
|
|
|
6386
6674
|
|
|
@@ -6393,7 +6681,8 @@ function shouldMinifyInnerHTML(options) {
|
|
|
6393
6681
|
options.removeOptionalTags ||
|
|
6394
6682
|
options.minifyJS !== identity ||
|
|
6395
6683
|
options.minifyCSS !== identityAsync ||
|
|
6396
|
-
options.minifyURLs !== identity
|
|
6684
|
+
options.minifyURLs !== identity ||
|
|
6685
|
+
options.minifySVG
|
|
6397
6686
|
);
|
|
6398
6687
|
}
|
|
6399
6688
|
|
|
@@ -6430,7 +6719,8 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
6430
6719
|
log: identity,
|
|
6431
6720
|
minifyCSS: identityAsync,
|
|
6432
6721
|
minifyJS: identity,
|
|
6433
|
-
minifyURLs: identity
|
|
6722
|
+
minifyURLs: identity,
|
|
6723
|
+
minifySVG: null
|
|
6434
6724
|
};
|
|
6435
6725
|
|
|
6436
6726
|
Object.keys(inputOptions).forEach(function (key) {
|
|
@@ -6663,6 +6953,11 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
6663
6953
|
return text;
|
|
6664
6954
|
}
|
|
6665
6955
|
};
|
|
6956
|
+
} else if (key === 'minifySVG') {
|
|
6957
|
+
// Process SVG minification options
|
|
6958
|
+
// Unlike minifyCSS/minifyJS, this is a simple options object, not a function
|
|
6959
|
+
// The actual minification is applied inline during attribute processing
|
|
6960
|
+
options.minifySVG = getSVGMinifierOptions(option);
|
|
6666
6961
|
} else {
|
|
6667
6962
|
options[key] = option;
|
|
6668
6963
|
}
|
|
@@ -7001,6 +7296,17 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
7001
7296
|
return attrValue;
|
|
7002
7297
|
}
|
|
7003
7298
|
return minifyHTMLSelf(attrValue, options, true);
|
|
7299
|
+
} else if (options.insideSVG && options.minifySVG) {
|
|
7300
|
+
// Apply SVG-specific attribute minification when inside SVG elements
|
|
7301
|
+
try {
|
|
7302
|
+
return minifySVGAttributeValue(attrName, attrValue, options.minifySVG);
|
|
7303
|
+
} catch (err) {
|
|
7304
|
+
if (!options.continueOnMinifyError) {
|
|
7305
|
+
throw err;
|
|
7306
|
+
}
|
|
7307
|
+
options.log && options.log(err);
|
|
7308
|
+
return attrValue;
|
|
7309
|
+
}
|
|
7004
7310
|
}
|
|
7005
7311
|
return attrValue;
|
|
7006
7312
|
}
|
|
@@ -7041,7 +7347,9 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
7041
7347
|
(options.removeScriptTypeAttributes && tag === 'script' &&
|
|
7042
7348
|
attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue)) ||
|
|
7043
7349
|
(options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
|
|
7044
|
-
attrName === 'type' && isStyleLinkTypeAttribute(attrValue))
|
|
7350
|
+
attrName === 'type' && isStyleLinkTypeAttribute(attrValue)) ||
|
|
7351
|
+
(options.insideSVG && options.minifySVG &&
|
|
7352
|
+
shouldRemoveSVGAttribute(attrName, attrValue, options.minifySVG))) {
|
|
7045
7353
|
return;
|
|
7046
7354
|
}
|
|
7047
7355
|
|
|
@@ -7611,6 +7919,16 @@ const jsMinifyCache = new LRU(200);
|
|
|
7611
7919
|
*
|
|
7612
7920
|
* Default: `false`
|
|
7613
7921
|
*
|
|
7922
|
+
* @prop {boolean | {precision?: number, removeDefaults?: boolean, minifyColors?: boolean}} [minifySVG]
|
|
7923
|
+
* When true, enables SVG-specific optimizations for SVG elements and attributes.
|
|
7924
|
+
* If an object is provided, it can include:
|
|
7925
|
+
* - `precision`: Number of decimal places for numeric values (coordinates, path data, etc.). Default: `3`
|
|
7926
|
+
* - `removeDefaults`: Remove attributes with default values (e.g., `fill="black"`). Default: `true`
|
|
7927
|
+
* - `minifyColors`: Minify color values (hex shortening, rgb to hex conversion). Default: `true`
|
|
7928
|
+
* If disabled, SVG content is minified using standard HTML rules only.
|
|
7929
|
+
*
|
|
7930
|
+
* Default: `false`
|
|
7931
|
+
*
|
|
7614
7932
|
* @prop {(name: string) => string} [name]
|
|
7615
7933
|
* Function used to normalise tag/attribute names. By default, this lowercases
|
|
7616
7934
|
* names, unless `caseSensitive` is enabled.
|
|
@@ -8205,6 +8523,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8205
8523
|
options.caseSensitive = true;
|
|
8206
8524
|
options.keepClosingSlash = true;
|
|
8207
8525
|
options.name = identity;
|
|
8526
|
+
options.insideSVG = lowerTag === 'svg';
|
|
8208
8527
|
}
|
|
8209
8528
|
tag = options.name(tag);
|
|
8210
8529
|
currentTag = tag;
|
|
@@ -245,6 +245,21 @@ export type MinifierOptions = {
|
|
|
245
245
|
* Default: `false`
|
|
246
246
|
*/
|
|
247
247
|
minifyURLs?: boolean | string | import("relateurl").Options | ((text: string) => Promise<string> | string);
|
|
248
|
+
/**
|
|
249
|
+
* When true, enables SVG-specific optimizations for SVG elements and attributes.
|
|
250
|
+
* If an object is provided, it can include:
|
|
251
|
+
* - `precision`: Number of decimal places for numeric values (coordinates, path data, etc.). Default: `3`
|
|
252
|
+
* - `removeDefaults`: Remove attributes with default values (e.g., `fill="black"`). Default: `true`
|
|
253
|
+
* - `minifyColors`: Minify color values (hex shortening, rgb to hex conversion). Default: `true`
|
|
254
|
+
* If disabled, SVG content is minified using standard HTML rules only.
|
|
255
|
+
*
|
|
256
|
+
* Default: `false`
|
|
257
|
+
*/
|
|
258
|
+
minifySVG?: boolean | {
|
|
259
|
+
precision?: number;
|
|
260
|
+
removeDefaults?: boolean;
|
|
261
|
+
minifyColors?: boolean;
|
|
262
|
+
};
|
|
248
263
|
/**
|
|
249
264
|
* Function used to normalise tag/attribute names. By default, this lowercases
|
|
250
265
|
* names, unless `caseSensitive` is enabled.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attributes.d.ts","sourceRoot":"","sources":["../../../src/lib/attributes.js"],"names":[],"mappings":"
|
|
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,0IA0IC;AAsBD;;;;GAwCC;AAED,6GA4EC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../../src/lib/options.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../../src/lib/options.js"],"names":[],"mappings":"AAWA,6DAUC;AAID;;;;;;;;;GASG;AACH,6CATW,OAAO,CAAC,eAAe,CAAC,0EAEhC;IAAuB,eAAe;IACf,SAAS;IACT,MAAM;CAA2B,GAG9C,eAAe,CA2Q3B"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minify SVG attribute value based on attribute name
|
|
3
|
+
* @param {string} name - Attribute name
|
|
4
|
+
* @param {string} value - Attribute value
|
|
5
|
+
* @param {Object} options - Minification options
|
|
6
|
+
* @returns {string} Minified attribute value
|
|
7
|
+
*/
|
|
8
|
+
export function minifySVGAttributeValue(name: string, value: string, options?: any): string;
|
|
9
|
+
/**
|
|
10
|
+
* Check if an SVG attribute can be removed
|
|
11
|
+
* @param {string} name - Attribute name
|
|
12
|
+
* @param {string} value - Attribute value
|
|
13
|
+
* @param {Object} options - Minification options
|
|
14
|
+
* @returns {boolean} True if attribute should be removed
|
|
15
|
+
*/
|
|
16
|
+
export function shouldRemoveSVGAttribute(name: string, value: string, options?: any): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Get default SVG minification options
|
|
19
|
+
* @param {Object} userOptions - User-provided options
|
|
20
|
+
* @returns {Object} Complete options object with defaults
|
|
21
|
+
*/
|
|
22
|
+
export function getSVGMinifierOptions(userOptions: any): any;
|
|
23
|
+
//# sourceMappingURL=svg.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"svg.d.ts","sourceRoot":"","sources":["../../../src/lib/svg.js"],"names":[],"mappings":"AAwMA;;;;;;GAMG;AACH,8CALW,MAAM,SACN,MAAM,kBAEJ,MAAM,CA0BlB;AAED;;;;;;GAMG;AACH,+CALW,MAAM,SACN,MAAM,kBAEJ,OAAO,CAQnB;AAED;;;;GAIG;AACH,6DAkBC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"whitespace.d.ts","sourceRoot":"","sources":["../../../src/lib/whitespace.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"whitespace.d.ts","sourceRoot":"","sources":["../../../src/lib/whitespace.js"],"names":[],"mappings":"AAgBA,8CAOC;AAID,qDAgBC;AAID,iHAyDC;AAID,0IAwBC;AAID,yDAEC;AAED,qDAEC"}
|
package/dist/types/presets.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export namespace presets {
|
|
|
37
37
|
export { decodeEntities_1 as decodeEntities };
|
|
38
38
|
export let minifyCSS: boolean;
|
|
39
39
|
export let minifyJS: boolean;
|
|
40
|
+
export let minifySVG: boolean;
|
|
40
41
|
let minifyURLs_1: boolean;
|
|
41
42
|
export { minifyURLs_1 as minifyURLs };
|
|
42
43
|
let noNewlinesBeforeTagClose_1: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../../src/presets.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../../src/presets.js"],"names":[],"mappings":"AAiDA;;;;GAIG;AACH,gCAHW,MAAM,GACJ,MAAM,GAAC,IAAI,CAMvB;AAED;;;GAGG;AACH,kCAFa,MAAM,EAAE,CAIpB"}
|
package/package.json
CHANGED
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
"description": "Super-configurable, well-tested, JavaScript-based HTML minifier (enhanced successor of HTML Minifier)",
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@commitlint/cli": "^20.2.0",
|
|
19
|
-
"@eslint/js": "^9.39.
|
|
19
|
+
"@eslint/js": "^9.39.2",
|
|
20
20
|
"@rollup/plugin-commonjs": "^29.0.0",
|
|
21
21
|
"@rollup/plugin-json": "^6.1.0",
|
|
22
22
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
23
23
|
"@rollup/plugin-terser": "^0.4.4",
|
|
24
24
|
"@swc/core": "^1.15.7",
|
|
25
|
-
"eslint": "^9.39.
|
|
25
|
+
"eslint": "^9.39.2",
|
|
26
26
|
"rollup": "^4.53.3",
|
|
27
27
|
"rollup-plugin-polyfill-node": "^0.13.0",
|
|
28
28
|
"typescript": "^5.9.3",
|
|
@@ -93,5 +93,5 @@
|
|
|
93
93
|
"test:watch": "node --test --watch tests/*.spec.js"
|
|
94
94
|
},
|
|
95
95
|
"type": "module",
|
|
96
|
-
"version": "4.
|
|
96
|
+
"version": "4.15.0"
|
|
97
97
|
}
|
package/src/htmlminifier.js
CHANGED
|
@@ -305,6 +305,16 @@ const jsMinifyCache = new LRU(200);
|
|
|
305
305
|
*
|
|
306
306
|
* Default: `false`
|
|
307
307
|
*
|
|
308
|
+
* @prop {boolean | {precision?: number, removeDefaults?: boolean, minifyColors?: boolean}} [minifySVG]
|
|
309
|
+
* When true, enables SVG-specific optimizations for SVG elements and attributes.
|
|
310
|
+
* If an object is provided, it can include:
|
|
311
|
+
* - `precision`: Number of decimal places for numeric values (coordinates, path data, etc.). Default: `3`
|
|
312
|
+
* - `removeDefaults`: Remove attributes with default values (e.g., `fill="black"`). Default: `true`
|
|
313
|
+
* - `minifyColors`: Minify color values (hex shortening, rgb to hex conversion). Default: `true`
|
|
314
|
+
* If disabled, SVG content is minified using standard HTML rules only.
|
|
315
|
+
*
|
|
316
|
+
* Default: `false`
|
|
317
|
+
*
|
|
308
318
|
* @prop {(name: string) => string} [name]
|
|
309
319
|
* Function used to normalise tag/attribute names. By default, this lowercases
|
|
310
320
|
* names, unless `caseSensitive` is enabled.
|
|
@@ -899,6 +909,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
899
909
|
options.caseSensitive = true;
|
|
900
910
|
options.keepClosingSlash = true;
|
|
901
911
|
options.name = identity;
|
|
912
|
+
options.insideSVG = lowerTag === 'svg';
|
|
902
913
|
}
|
|
903
914
|
tag = options.name(tag);
|
|
904
915
|
currentTag = tag;
|
package/src/htmlparser.js
CHANGED
|
@@ -185,7 +185,7 @@ export class HTMLParser {
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
// https://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
|
|
188
|
+
// https://web.archive.org/web/20241201212701/https://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
|
|
189
189
|
if (/^<!\[/.test(html)) {
|
|
190
190
|
const conditionalEnd = html.indexOf(']>');
|
|
191
191
|
|
package/src/lib/attributes.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from './constants.js';
|
|
18
18
|
import { trimWhitespace, collapseWhitespaceAll } from './whitespace.js';
|
|
19
19
|
import { shouldMinifyInnerHTML } from './options.js';
|
|
20
|
+
import { minifySVGAttributeValue, shouldRemoveSVGAttribute } from './svg.js';
|
|
20
21
|
|
|
21
22
|
// Validators
|
|
22
23
|
|
|
@@ -346,6 +347,17 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
346
347
|
return attrValue;
|
|
347
348
|
}
|
|
348
349
|
return minifyHTMLSelf(attrValue, options, true);
|
|
350
|
+
} else if (options.insideSVG && options.minifySVG) {
|
|
351
|
+
// Apply SVG-specific attribute minification when inside SVG elements
|
|
352
|
+
try {
|
|
353
|
+
return minifySVGAttributeValue(attrName, attrValue, options.minifySVG);
|
|
354
|
+
} catch (err) {
|
|
355
|
+
if (!options.continueOnMinifyError) {
|
|
356
|
+
throw err;
|
|
357
|
+
}
|
|
358
|
+
options.log && options.log(err);
|
|
359
|
+
return attrValue;
|
|
360
|
+
}
|
|
349
361
|
}
|
|
350
362
|
return attrValue;
|
|
351
363
|
}
|
|
@@ -386,7 +398,9 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
386
398
|
(options.removeScriptTypeAttributes && tag === 'script' &&
|
|
387
399
|
attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue)) ||
|
|
388
400
|
(options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
|
|
389
|
-
attrName === 'type' && isStyleLinkTypeAttribute(attrValue))
|
|
401
|
+
attrName === 'type' && isStyleLinkTypeAttribute(attrValue)) ||
|
|
402
|
+
(options.insideSVG && options.minifySVG &&
|
|
403
|
+
shouldRemoveSVGAttribute(attrName, attrValue, options.minifySVG))) {
|
|
390
404
|
return;
|
|
391
405
|
}
|
|
392
406
|
|
package/src/lib/options.js
CHANGED
|
@@ -5,6 +5,7 @@ import { stableStringify, identity, identityAsync, replaceAsync } from './utils.
|
|
|
5
5
|
import { RE_TRAILING_SEMICOLON } from './constants.js';
|
|
6
6
|
import { canCollapseWhitespace, canTrimWhitespace } from './whitespace.js';
|
|
7
7
|
import { wrapCSS, unwrapCSS } from './content.js';
|
|
8
|
+
import { getSVGMinifierOptions } from './svg.js';
|
|
8
9
|
|
|
9
10
|
// Helper functions
|
|
10
11
|
|
|
@@ -15,7 +16,8 @@ function shouldMinifyInnerHTML(options) {
|
|
|
15
16
|
options.removeOptionalTags ||
|
|
16
17
|
options.minifyJS !== identity ||
|
|
17
18
|
options.minifyCSS !== identityAsync ||
|
|
18
|
-
options.minifyURLs !== identity
|
|
19
|
+
options.minifyURLs !== identity ||
|
|
20
|
+
options.minifySVG
|
|
19
21
|
);
|
|
20
22
|
}
|
|
21
23
|
|
|
@@ -52,7 +54,8 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
52
54
|
log: identity,
|
|
53
55
|
minifyCSS: identityAsync,
|
|
54
56
|
minifyJS: identity,
|
|
55
|
-
minifyURLs: identity
|
|
57
|
+
minifyURLs: identity,
|
|
58
|
+
minifySVG: null
|
|
56
59
|
};
|
|
57
60
|
|
|
58
61
|
Object.keys(inputOptions).forEach(function (key) {
|
|
@@ -285,6 +288,11 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
285
288
|
return text;
|
|
286
289
|
}
|
|
287
290
|
};
|
|
291
|
+
} else if (key === 'minifySVG') {
|
|
292
|
+
// Process SVG minification options
|
|
293
|
+
// Unlike minifyCSS/minifyJS, this is a simple options object, not a function
|
|
294
|
+
// The actual minification is applied inline during attribute processing
|
|
295
|
+
options.minifySVG = getSVGMinifierOptions(option);
|
|
288
296
|
} else {
|
|
289
297
|
options[key] = option;
|
|
290
298
|
}
|