html-minifier-next 4.14.3 → 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.
@@ -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;
@@ -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":"AA60CO,8BAJI,MAAM,YACN,eAAe,GACb,OAAO,CAAC,MAAM,CAAC,CAc3B;;;;;;;;;;;;UAnvCS,MAAM;YACN,MAAM;YACN,MAAM;mBACN,MAAM;iBACN,MAAM;kBACN,MAAM;;;;;;;;;;;;;4BAQN,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,qBAAqB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,KAAK,OAAO;;;;;;;wBAMjG,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,SAAS,EAAE,iBAAiB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,KAAK,OAAO;;;;;;;;oBAMhH,OAAO;;;;;;;;;kCAON,OAAO;;;;;;;;gCAQR,OAAO;;;;;;;;kCAOP,OAAO;;;;;;;;yBAOP,OAAO;;;;;;;;2BAOP,OAAO;;;;;;;;4BAOP,OAAO;;;;;;;2BAOP,OAAO;;;;;;;;uBAMP,MAAM,EAAE;;;;;;yBAOR,MAAM;;;;;;yBAKN,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE;;;;;;;4BAKlB,MAAM,EAAE;;;;;;;oCAMR,MAAM;;;;;;;qBAMN,OAAO;;;;;;;YAMP,OAAO;;;;;;;;2BAMP,MAAM,EAAE;;;;;;;;;4BAOR,MAAM,EAAE;;;;;;;+BAQR,OAAO;;;;;;;2BAMP,SAAS,CAAC,MAAM,CAAC;;;;;;uBAMjB,OAAO;;;;;;;;UAKP,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI;;;;;;;;qBAO1B,MAAM;;;;;;;oBAON,MAAM;;;;;;;;;;gBAMN,OAAO,GAAG,OAAO,CAAC,OAAO,cAAc,EAAE,gBAAgB,CAAC,OAAO,cAAc,EAAE,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;;;;;;;;;;;;;;eAS9J,OAAO,GAAG,OAAO,QAAQ,EAAE,aAAa,GAAG;QAAC,MAAM,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAC,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;;;;;;;;;;iBAa3J,OAAO,GAAG,MAAM,GAAG,OAAO,WAAW,EAAE,OAAO,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;;;;;;;;WAS7F,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM;;;;;;;+BAOxB,OAAO;;;;;;;;;;oBAMP,OAAO;;;;;;;;yBASP,OAAO;;;;;;;gCAOP,OAAO;;;;;;;;iCAMP,OAAO;;;;;;;;;;qBAOP,MAAM,EAAE;;;;;;;qBASR,IAAI,GAAG,GAAG;;;;;;;4BAMV,OAAO;;;;;;;;qBAMP,OAAO;;;;;;;;;4BAOP,OAAO,GAAG,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;;;;;;;;0BAQtD,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;gCAOP,MAAM,EAAE;;;;;;;;yBAyBR,OAAO;;;;;;;;gCAOP,OAAO;;;;;;;iCAOP,OAAO;;;;;;;oCAMP,OAAO;;;;;;;;;;0BAMP,OAAO;;;;;;;;;qBASP,OAAO,GAAG,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,IAAI,CAAC;;;;;;;;;oBAQzD,OAAO,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;;;;;;;;0BAQrC,OAAO;;;;;;;sBAOP,OAAO;;wBAtdkC,cAAc;0BAAd,cAAc;+BAAd,cAAc"}
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":"AAsBA,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,0IA+HC;AAsBD;;;;GAsCC;AAED,6GA4EC"}
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":"AAUA,6DASC;AAID;;;;;;;;;GASG;AACH,6CATW,OAAO,CAAC,eAAe,CAAC,0EAEhC;IAAuB,eAAe;IACf,SAAS;IACT,MAAM;CAA2B,GAG9C,eAAe,CAqQ3B"}
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"}
@@ -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":"AAgDA;;;;GAIG;AACH,gCAHW,MAAM,GACJ,MAAM,GAAC,IAAI,CAMvB;AAED;;;GAGG;AACH,kCAFa,MAAM,EAAE,CAIpB"}
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.1",
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.1",
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.14.3"
96
+ "version": "4.15.0"
97
97
  }
@@ -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
 
@@ -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
 
@@ -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
  }