html-minifier-next 4.14.3 → 4.15.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 +74 -20
- package/cli.js +3 -2
- package/dist/htmlminifier.cjs +311 -7
- package/dist/htmlminifier.esm.bundle.js +311 -7
- 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/presets.d.ts +1 -0
- package/dist/types/presets.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/htmlminifier.js +13 -3
- 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/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,
|
|
@@ -6395,6 +6396,279 @@ async function processScript(text, options, currentAttrs, minifyHTML) {
|
|
|
6395
6396
|
return text;
|
|
6396
6397
|
}
|
|
6397
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
|
+
|
|
6398
6672
|
// Imports
|
|
6399
6673
|
|
|
6400
6674
|
|
|
@@ -6407,7 +6681,8 @@ function shouldMinifyInnerHTML(options) {
|
|
|
6407
6681
|
options.removeOptionalTags ||
|
|
6408
6682
|
options.minifyJS !== identity ||
|
|
6409
6683
|
options.minifyCSS !== identityAsync ||
|
|
6410
|
-
options.minifyURLs !== identity
|
|
6684
|
+
options.minifyURLs !== identity ||
|
|
6685
|
+
options.minifySVG
|
|
6411
6686
|
);
|
|
6412
6687
|
}
|
|
6413
6688
|
|
|
@@ -6444,7 +6719,8 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
6444
6719
|
log: identity,
|
|
6445
6720
|
minifyCSS: identityAsync,
|
|
6446
6721
|
minifyJS: identity,
|
|
6447
|
-
minifyURLs: identity
|
|
6722
|
+
minifyURLs: identity,
|
|
6723
|
+
minifySVG: null
|
|
6448
6724
|
};
|
|
6449
6725
|
|
|
6450
6726
|
Object.keys(inputOptions).forEach(function (key) {
|
|
@@ -6677,6 +6953,11 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
6677
6953
|
return text;
|
|
6678
6954
|
}
|
|
6679
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);
|
|
6680
6961
|
} else {
|
|
6681
6962
|
options[key] = option;
|
|
6682
6963
|
}
|
|
@@ -7015,6 +7296,17 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
7015
7296
|
return attrValue;
|
|
7016
7297
|
}
|
|
7017
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
|
+
}
|
|
7018
7310
|
}
|
|
7019
7311
|
return attrValue;
|
|
7020
7312
|
}
|
|
@@ -7055,7 +7347,9 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
7055
7347
|
(options.removeScriptTypeAttributes && tag === 'script' &&
|
|
7056
7348
|
attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue)) ||
|
|
7057
7349
|
(options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
|
|
7058
|
-
attrName === 'type' && isStyleLinkTypeAttribute(attrValue))
|
|
7350
|
+
attrName === 'type' && isStyleLinkTypeAttribute(attrValue)) ||
|
|
7351
|
+
(options.insideSVG && options.minifySVG &&
|
|
7352
|
+
shouldRemoveSVGAttribute(attrName, attrValue, options.minifySVG))) {
|
|
7059
7353
|
return;
|
|
7060
7354
|
}
|
|
7061
7355
|
|
|
@@ -7625,6 +7919,16 @@ const jsMinifyCache = new LRU(200);
|
|
|
7625
7919
|
*
|
|
7626
7920
|
* Default: `false`
|
|
7627
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
|
+
*
|
|
7628
7932
|
* @prop {(name: string) => string} [name]
|
|
7629
7933
|
* Function used to normalise tag/attribute names. By default, this lowercases
|
|
7630
7934
|
* names, unless `caseSensitive` is enabled.
|
|
@@ -8219,6 +8523,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8219
8523
|
options.caseSensitive = true;
|
|
8220
8524
|
options.keepClosingSlash = true;
|
|
8221
8525
|
options.name = identity;
|
|
8526
|
+
options.insideSVG = lowerTag === 'svg';
|
|
8222
8527
|
}
|
|
8223
8528
|
tag = options.name(tag);
|
|
8224
8529
|
currentTag = tag;
|
|
@@ -8337,7 +8642,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8337
8642
|
// `</head>` may be omitted if not followed by space or comment
|
|
8338
8643
|
// `</p>` may be omitted if no more content in non-`</a>` parent
|
|
8339
8644
|
// except for `</dt>` or `</thead>`, end tags may be omitted if no more content in parent element
|
|
8340
|
-
if (
|
|
8645
|
+
if (tag && optionalEndTag && !trailingTags.has(optionalEndTag) && (optionalEndTag !== 'p' || !pInlineTags.has(tag))) {
|
|
8341
8646
|
removeEndTag();
|
|
8342
8647
|
}
|
|
8343
8648
|
optionalEndTag = optionalEndTags.has(tag) ? tag : '';
|
|
@@ -8515,8 +8820,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
8515
8820
|
const prevComment = buffer[buffer.length - 2];
|
|
8516
8821
|
|
|
8517
8822
|
// Check if previous item is whitespace-only and item before that is ignore-placeholder
|
|
8518
|
-
if (prevText && /^\s+$/.test(prevText) &&
|
|
8519
|
-
prevComment && uidIgnorePlaceholderPattern.test(prevComment)) {
|
|
8823
|
+
if (prevText && /^\s+$/.test(prevText) && prevComment && uidIgnorePlaceholderPattern.test(prevComment)) {
|
|
8520
8824
|
// Extract the index from both placeholders to check their content
|
|
8521
8825
|
const currentMatch = text.match(uidIgnorePlaceholderPattern);
|
|
8522
8826
|
const prevMatch = prevComment.match(uidIgnorePlaceholderPattern);
|
|
@@ -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"}
|
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.1"
|
|
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;
|
|
@@ -1017,7 +1028,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1017
1028
|
// `</head>` may be omitted if not followed by space or comment
|
|
1018
1029
|
// `</p>` may be omitted if no more content in non-`</a>` parent
|
|
1019
1030
|
// except for `</dt>` or `</thead>`, end tags may be omitted if no more content in parent element
|
|
1020
|
-
if (
|
|
1031
|
+
if (tag && optionalEndTag && !trailingTags.has(optionalEndTag) && (optionalEndTag !== 'p' || !pInlineTags.has(tag))) {
|
|
1021
1032
|
removeEndTag();
|
|
1022
1033
|
}
|
|
1023
1034
|
optionalEndTag = optionalEndTags.has(tag) ? tag : '';
|
|
@@ -1195,8 +1206,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1195
1206
|
const prevComment = buffer[buffer.length - 2];
|
|
1196
1207
|
|
|
1197
1208
|
// Check if previous item is whitespace-only and item before that is ignore-placeholder
|
|
1198
|
-
if (prevText && /^\s+$/.test(prevText) &&
|
|
1199
|
-
prevComment && uidIgnorePlaceholderPattern.test(prevComment)) {
|
|
1209
|
+
if (prevText && /^\s+$/.test(prevText) && prevComment && uidIgnorePlaceholderPattern.test(prevComment)) {
|
|
1200
1210
|
// Extract the index from both placeholders to check their content
|
|
1201
1211
|
const currentMatch = text.match(uidIgnorePlaceholderPattern);
|
|
1202
1212
|
const prevMatch = prevComment.match(uidIgnorePlaceholderPattern);
|
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
|
}
|