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
package/dist/htmlminifier.cjs
CHANGED
|
@@ -1034,7 +1034,6 @@ const RE_ESCAPE_LT = /</g;
|
|
|
1034
1034
|
const RE_ATTR_WS_CHECK = /[ \n\r\t\f]/;
|
|
1035
1035
|
const RE_ATTR_WS_COLLAPSE = /[ \n\r\t\f]+/g;
|
|
1036
1036
|
const RE_ATTR_WS_TRIM = /^[ \n\r\t\f]+|[ \n\r\t\f]+$/g;
|
|
1037
|
-
const RE_NUMERIC_VALUE = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
|
|
1038
1037
|
|
|
1039
1038
|
// Inline element sets for whitespace handling
|
|
1040
1039
|
|
|
@@ -1638,427 +1637,6 @@ async function processScript(text, options, currentAttrs, minifyHTML) {
|
|
|
1638
1637
|
return text;
|
|
1639
1638
|
}
|
|
1640
1639
|
|
|
1641
|
-
/**
|
|
1642
|
-
* Lightweight SVG optimizations:
|
|
1643
|
-
* - Numeric precision reduction for coordinates and path data
|
|
1644
|
-
* - Whitespace removal in attribute values (numeric sequences)
|
|
1645
|
-
* - Default attribute removal (safe, well-documented defaults)
|
|
1646
|
-
* - Color minification (hex shortening, rgb() to hex, named colors)
|
|
1647
|
-
* - Identity transform removal
|
|
1648
|
-
* - Path data space optimization
|
|
1649
|
-
*/
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
// Cache for minified numbers
|
|
1653
|
-
const numberCache = new LRU(100);
|
|
1654
|
-
|
|
1655
|
-
/**
|
|
1656
|
-
* Named colors that are shorter than their hex equivalents
|
|
1657
|
-
* Only includes cases where using the name saves bytes
|
|
1658
|
-
*/
|
|
1659
|
-
const NAMED_COLORS = {
|
|
1660
|
-
'#f00': 'red', // #f00 (4) → red (3), saves 1
|
|
1661
|
-
'#c0c0c0': 'silver', // #c0c0c0 (7) → silver (6), saves 1
|
|
1662
|
-
'#808080': 'gray', // #808080 (7) → gray (4), saves 3
|
|
1663
|
-
'#800000': 'maroon', // #800000 (7) → maroon (6), saves 1
|
|
1664
|
-
'#808000': 'olive', // #808000 (7) → olive (5), saves 2
|
|
1665
|
-
'#008000': 'green', // #008000 (7) → green (5), saves 2
|
|
1666
|
-
'#800080': 'purple', // #800080 (7) → purple (6), saves 1
|
|
1667
|
-
'#008080': 'teal', // #008080 (7) → teal (4), saves 3
|
|
1668
|
-
'#000080': 'navy', // #000080 (7) → navy (4), saves 3
|
|
1669
|
-
'#ffa500': 'orange' // #ffa500 (7) → orange (6), saves 1
|
|
1670
|
-
};
|
|
1671
|
-
|
|
1672
|
-
/**
|
|
1673
|
-
* Default SVG attribute values that can be safely removed
|
|
1674
|
-
* Only includes well-documented, widely-supported defaults
|
|
1675
|
-
*/
|
|
1676
|
-
const SVG_DEFAULT_ATTRS = {
|
|
1677
|
-
// Fill and stroke defaults
|
|
1678
|
-
fill: value => value === 'black' || value === '#000' || value === '#000000',
|
|
1679
|
-
'fill-opacity': value => value === '1',
|
|
1680
|
-
'fill-rule': value => value === 'nonzero',
|
|
1681
|
-
stroke: value => value === 'none',
|
|
1682
|
-
'stroke-dasharray': value => value === 'none',
|
|
1683
|
-
'stroke-dashoffset': value => value === '0',
|
|
1684
|
-
'stroke-linecap': value => value === 'butt',
|
|
1685
|
-
'stroke-linejoin': value => value === 'miter',
|
|
1686
|
-
'stroke-miterlimit': value => value === '4',
|
|
1687
|
-
'stroke-opacity': value => value === '1',
|
|
1688
|
-
'stroke-width': value => value === '1',
|
|
1689
|
-
|
|
1690
|
-
// Text and font defaults
|
|
1691
|
-
'font-family': value => value === 'inherit',
|
|
1692
|
-
'font-size': value => value === 'medium',
|
|
1693
|
-
'font-style': value => value === 'normal',
|
|
1694
|
-
'font-variant': value => value === 'normal',
|
|
1695
|
-
'font-weight': value => value === 'normal',
|
|
1696
|
-
'letter-spacing': value => value === 'normal',
|
|
1697
|
-
'text-decoration': value => value === 'none',
|
|
1698
|
-
'text-anchor': value => value === 'start',
|
|
1699
|
-
|
|
1700
|
-
// Other common defaults
|
|
1701
|
-
opacity: value => value === '1',
|
|
1702
|
-
visibility: value => value === 'visible',
|
|
1703
|
-
display: value => value === 'inline',
|
|
1704
|
-
// Note: Overflow handled especially in `isDefaultAttribute` (not safe for root `<svg>`)
|
|
1705
|
-
|
|
1706
|
-
// Clipping and masking defaults
|
|
1707
|
-
'clip-rule': value => value === 'nonzero',
|
|
1708
|
-
'clip-path': value => value === 'none',
|
|
1709
|
-
mask: value => value === 'none',
|
|
1710
|
-
|
|
1711
|
-
// Marker defaults
|
|
1712
|
-
'marker-start': value => value === 'none',
|
|
1713
|
-
'marker-mid': value => value === 'none',
|
|
1714
|
-
'marker-end': value => value === 'none',
|
|
1715
|
-
|
|
1716
|
-
// Filter and color defaults
|
|
1717
|
-
filter: value => value === 'none',
|
|
1718
|
-
'color-interpolation': value => value === 'sRGB',
|
|
1719
|
-
'color-interpolation-filters': value => value === 'linearRGB'
|
|
1720
|
-
};
|
|
1721
|
-
|
|
1722
|
-
/**
|
|
1723
|
-
* Minify numeric value by removing trailing zeros and unnecessary decimals
|
|
1724
|
-
* @param {string} num - Numeric string to minify
|
|
1725
|
-
* @param {number} precision - Maximum decimal places to keep
|
|
1726
|
-
* @returns {string} Minified numeric string
|
|
1727
|
-
*/
|
|
1728
|
-
function minifyNumber(num, precision = 3) {
|
|
1729
|
-
// Fast path for common values (avoids parsing and caching)
|
|
1730
|
-
if (num === '0' || num === '1') return num;
|
|
1731
|
-
// Common decimal variants that tools export
|
|
1732
|
-
if (num === '0.0' || num === '0.00' || num === '0.000') return '0';
|
|
1733
|
-
if (num === '1.0' || num === '1.00' || num === '1.000') return '1';
|
|
1734
|
-
|
|
1735
|
-
// Check cache
|
|
1736
|
-
// (Note: Uses input string as key, so “0.0000” and “0.00000” create separate entries.
|
|
1737
|
-
// This is intentional to avoid parsing overhead.
|
|
1738
|
-
// Real-world SVG files from export tools typically use consistent formats.)
|
|
1739
|
-
const cacheKey = `${num}:${precision}`;
|
|
1740
|
-
const cached = numberCache.get(cacheKey);
|
|
1741
|
-
if (cached !== undefined) return cached;
|
|
1742
|
-
|
|
1743
|
-
const parsed = parseFloat(num);
|
|
1744
|
-
|
|
1745
|
-
// Handle special cases
|
|
1746
|
-
if (isNaN(parsed)) return num;
|
|
1747
|
-
if (parsed === 0) return '0';
|
|
1748
|
-
if (!isFinite(parsed)) return num;
|
|
1749
|
-
|
|
1750
|
-
// Convert to fixed precision, then remove trailing zeros
|
|
1751
|
-
const fixed = parsed.toFixed(precision);
|
|
1752
|
-
const trimmed = fixed.replace(/\.?0+$/, '');
|
|
1753
|
-
|
|
1754
|
-
// Remove leading zero before decimal point (e.g., `0.5` → `.5`, `-0.3` → `-.3`)
|
|
1755
|
-
const result = (trimmed || '0').replace(/^(-?)0\./, '$1.');
|
|
1756
|
-
numberCache.set(cacheKey, result);
|
|
1757
|
-
return result;
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
/**
|
|
1761
|
-
* Minify SVG path data by reducing numeric precision and removing unnecessary spaces
|
|
1762
|
-
* @param {string} pathData - SVG path data string
|
|
1763
|
-
* @param {number} precision - Decimal precision for coordinates
|
|
1764
|
-
* @returns {string} Minified path data
|
|
1765
|
-
*/
|
|
1766
|
-
function minifyPathData(pathData, precision = 3) {
|
|
1767
|
-
if (!pathData || typeof pathData !== 'string') return pathData;
|
|
1768
|
-
|
|
1769
|
-
// First, minify all numbers
|
|
1770
|
-
let result = pathData.replace(RE_NUMERIC_VALUE, (match) => {
|
|
1771
|
-
return minifyNumber(match, precision);
|
|
1772
|
-
});
|
|
1773
|
-
|
|
1774
|
-
// Remove unnecessary spaces around path commands
|
|
1775
|
-
// Safe to remove space after a command letter when it’s followed by a number
|
|
1776
|
-
// (which may be negative or start with a decimal point)
|
|
1777
|
-
// `M 10 20` → `M10 20`, `L -5 -3` → `L-5-3`, `M .5 .3` → `M.5.3`
|
|
1778
|
-
result = result.replace(/([MLHVCSQTAZmlhvcsqtaz])\s+(?=-?\.?\d)/g, '$1');
|
|
1779
|
-
|
|
1780
|
-
// Safe to remove space before command letter when preceded by a number
|
|
1781
|
-
// `0 L` → `0L`, `20 M` → `20M`, `.5 L` → `.5L`
|
|
1782
|
-
result = result.replace(/([\d.])\s+([MLHVCSQTAZmlhvcsqtaz])/g, '$1$2');
|
|
1783
|
-
|
|
1784
|
-
// Safe to remove space before negative number when preceded by a number
|
|
1785
|
-
// `10 -20` → `10-20`, `.5 -.3` → `.5-.3` (minus sign is always a separator)
|
|
1786
|
-
result = result.replace(/([\d.])\s+(-)/g, '$1$2');
|
|
1787
|
-
|
|
1788
|
-
// Safe to remove space between two decimal numbers (decimal point acts as separator)
|
|
1789
|
-
// `.5 .3` → `.5.3` (only when previous char is `.`, indicating a complete decimal)
|
|
1790
|
-
// Note: `0 .3` must not become `0.3` (that would change two numbers into one)
|
|
1791
|
-
result = result.replace(/(\.\d*)\s+(\.)/g, '$1$2');
|
|
1792
|
-
|
|
1793
|
-
return result;
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
/**
|
|
1797
|
-
* Minify whitespace in numeric attribute values
|
|
1798
|
-
* Examples:
|
|
1799
|
-
* - “10 , 20" → "10,20"
|
|
1800
|
-
* - "translate( 10 20 )" → "translate(10 20)"
|
|
1801
|
-
* - "100, 10 40, 198" → "100,10 40,198"
|
|
1802
|
-
*
|
|
1803
|
-
* @param {string} value - Attribute value to minify
|
|
1804
|
-
* @returns {string} Minified value
|
|
1805
|
-
*/
|
|
1806
|
-
function minifyAttributeWhitespace(value) {
|
|
1807
|
-
if (!value || typeof value !== 'string') return value;
|
|
1808
|
-
|
|
1809
|
-
return value
|
|
1810
|
-
// Remove spaces around commas
|
|
1811
|
-
.replace(/\s*,\s*/g, ',')
|
|
1812
|
-
// Remove spaces around parentheses
|
|
1813
|
-
.replace(/\(\s+/g, '(')
|
|
1814
|
-
.replace(/\s+\)/g, ')')
|
|
1815
|
-
// Collapse multiple spaces to single space
|
|
1816
|
-
.replace(/\s+/g, ' ')
|
|
1817
|
-
// Trim leading/trailing whitespace
|
|
1818
|
-
.trim();
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
/**
|
|
1822
|
-
* Minify color values (hex shortening, rgb to hex conversion, named colors)
|
|
1823
|
-
* Only processes simple color values; preserves case-sensitive references like `url(#id)`
|
|
1824
|
-
* @param {string} color - Color value to minify
|
|
1825
|
-
* @returns {string} Minified color value
|
|
1826
|
-
*/
|
|
1827
|
-
function minifyColor(color) {
|
|
1828
|
-
if (!color || typeof color !== 'string') return color;
|
|
1829
|
-
|
|
1830
|
-
const trimmed = color.trim();
|
|
1831
|
-
|
|
1832
|
-
// Don’t process values that aren’t simple colors (preserve case-sensitive references)
|
|
1833
|
-
// `url(#id)`, `var(--name)`, `inherit`, `currentColor`, etc.
|
|
1834
|
-
if (trimmed.includes('url(') || trimmed.includes('var(') || trimmed === 'inherit' || trimmed === 'currentColor') {
|
|
1835
|
-
return trimmed;
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
// Now safe to lowercase for color matching
|
|
1839
|
-
const lower = trimmed.toLowerCase();
|
|
1840
|
-
|
|
1841
|
-
// Shorten 6-digit hex to 3-digit when possible
|
|
1842
|
-
// `#aabbcc` → `#abc`, `#000000` → `#000`
|
|
1843
|
-
const hexMatch = lower.match(/^#([0-9a-f]{6})$/);
|
|
1844
|
-
if (hexMatch) {
|
|
1845
|
-
const hex = hexMatch[1];
|
|
1846
|
-
if (hex[0] === hex[1] && hex[2] === hex[3] && hex[4] === hex[5]) {
|
|
1847
|
-
const shortened = '#' + hex[0] + hex[2] + hex[4];
|
|
1848
|
-
// Try to use named color if shorter
|
|
1849
|
-
return NAMED_COLORS[shortened] || shortened;
|
|
1850
|
-
}
|
|
1851
|
-
// Can’t shorten, but check for named color
|
|
1852
|
-
return NAMED_COLORS[lower] || lower;
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
|
-
// Match 3-digit hex colors
|
|
1856
|
-
const hex3Match = lower.match(/^#[0-9a-f]{3}$/);
|
|
1857
|
-
if (hex3Match) {
|
|
1858
|
-
// Check if there’s a shorter named color
|
|
1859
|
-
return NAMED_COLORS[lower] || lower;
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
|
-
// Convert rgb() to hex
|
|
1863
|
-
const rgbMatch = lower.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
|
|
1864
|
-
if (rgbMatch) {
|
|
1865
|
-
const r = parseInt(rgbMatch[1], 10);
|
|
1866
|
-
const g = parseInt(rgbMatch[2], 10);
|
|
1867
|
-
const b = parseInt(rgbMatch[3], 10);
|
|
1868
|
-
|
|
1869
|
-
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
|
|
1870
|
-
const toHex = (n) => {
|
|
1871
|
-
const h = n.toString(16);
|
|
1872
|
-
return h.length === 1 ? '0' + h : h;
|
|
1873
|
-
};
|
|
1874
|
-
const hexColor = '#' + toHex(r) + toHex(g) + toHex(b);
|
|
1875
|
-
|
|
1876
|
-
// Try to shorten if possible
|
|
1877
|
-
if (hexColor[1] === hexColor[2] && hexColor[3] === hexColor[4] && hexColor[5] === hexColor[6]) {
|
|
1878
|
-
const shortened = '#' + hexColor[1] + hexColor[3] + hexColor[5];
|
|
1879
|
-
return NAMED_COLORS[shortened] || shortened;
|
|
1880
|
-
}
|
|
1881
|
-
return NAMED_COLORS[hexColor] || hexColor;
|
|
1882
|
-
}
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
|
-
// Not a recognized color format, return as-is (preserves case)
|
|
1886
|
-
return trimmed;
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1889
|
-
// Attributes that contain numeric sequences or path data
|
|
1890
|
-
const NUMERIC_ATTRS = new Set([
|
|
1891
|
-
'd', // Path data
|
|
1892
|
-
'points', // Polygon/polyline points
|
|
1893
|
-
'viewBox', // `viewBox` coordinates
|
|
1894
|
-
'transform', // Transform functions
|
|
1895
|
-
'x', 'y', 'x1', 'y1', 'x2', 'y2', // Coordinates
|
|
1896
|
-
'cx', 'cy', 'r', 'rx', 'ry', // Circle/ellipse
|
|
1897
|
-
'width', 'height', // Dimensions
|
|
1898
|
-
'dx', 'dy', // Text offsets
|
|
1899
|
-
'offset', // Gradient offset
|
|
1900
|
-
'startOffset', // `textPath`
|
|
1901
|
-
'pathLength', // Path length
|
|
1902
|
-
'stdDeviation', // Filter params
|
|
1903
|
-
'baseFrequency', // Turbulence
|
|
1904
|
-
'k1', 'k2', 'k3', 'k4' // Composite filter
|
|
1905
|
-
]);
|
|
1906
|
-
|
|
1907
|
-
// Attributes that contain color values
|
|
1908
|
-
const COLOR_ATTRS = new Set([
|
|
1909
|
-
'fill',
|
|
1910
|
-
'stroke',
|
|
1911
|
-
'stop-color',
|
|
1912
|
-
'flood-color',
|
|
1913
|
-
'lighting-color'
|
|
1914
|
-
]);
|
|
1915
|
-
|
|
1916
|
-
// Pre-compiled regexes for identity transform detection (compiled once at module load)
|
|
1917
|
-
// Separator pattern: Accepts comma with optional spaces or one or more spaces
|
|
1918
|
-
const SEP = '(?:\\s*,\\s*|\\s+)';
|
|
1919
|
-
|
|
1920
|
-
// `translate(0)`, `translate(0,0)`, `translate(0 0)` (matches 0, 0.0, 0.00, etc.)
|
|
1921
|
-
const IDENTITY_TRANSLATE_RE = new RegExp(`^translate\\s*\\(\\s*0(?:\\.0+)?\\s*(?:${SEP}0(?:\\.0+)?\\s*)?\\)$`, 'i');
|
|
1922
|
-
|
|
1923
|
-
// `scale(1)`, `scale(1,1)`, `scale(1 1)` (matches 1, 1.0, 1.00, etc.)
|
|
1924
|
-
const IDENTITY_SCALE_RE = new RegExp(`^scale\\s*\\(\\s*1(?:\\.0+)?\\s*(?:${SEP}1(?:\\.0+)?\\s*)?\\)$`, 'i');
|
|
1925
|
-
|
|
1926
|
-
// `rotate(0)`, `rotate(0 cx cy)`, `rotate(0, cx, cy)` (matches 0, 0.0, 0.00, etc.)
|
|
1927
|
-
// Note: `cx` and `cy` must be valid numbers if present
|
|
1928
|
-
const IDENTITY_ROTATE_RE = new RegExp(`^rotate\\s*\\(\\s*0(?:\\.0+)?\\s*(?:${SEP}-?\\d+(?:\\.\\d+)?${SEP}-?\\d+(?:\\.\\d+)?)?\\s*\\)$`, 'i');
|
|
1929
|
-
|
|
1930
|
-
// `skewX(0)`, `skewY(0)` (matches 0, 0.0, 0.00, etc.)
|
|
1931
|
-
const IDENTITY_SKEW_RE = /^skew[XY]\s*\(\s*0(?:\.0+)?\s*\)$/i;
|
|
1932
|
-
|
|
1933
|
-
// `matrix(1,0,0,1,0,0)`, `matrix(1 0 0 1 0 0)`—identity matrix (matches 1.0/0.0 variants)
|
|
1934
|
-
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');
|
|
1935
|
-
|
|
1936
|
-
/**
|
|
1937
|
-
* Check if a transform attribute has no effect (identity transform)
|
|
1938
|
-
* @param {string} transform - Transform attribute value
|
|
1939
|
-
* @returns {boolean} True if transform is an identity (has no effect)
|
|
1940
|
-
*/
|
|
1941
|
-
function isIdentityTransform(transform) {
|
|
1942
|
-
if (!transform || typeof transform !== 'string') return false;
|
|
1943
|
-
|
|
1944
|
-
const trimmed = transform.trim();
|
|
1945
|
-
|
|
1946
|
-
// Check for common identity transforms using pre-compiled regexes
|
|
1947
|
-
return IDENTITY_TRANSLATE_RE.test(trimmed) ||
|
|
1948
|
-
IDENTITY_SCALE_RE.test(trimmed) ||
|
|
1949
|
-
IDENTITY_ROTATE_RE.test(trimmed) ||
|
|
1950
|
-
IDENTITY_SKEW_RE.test(trimmed) ||
|
|
1951
|
-
IDENTITY_MATRIX_RE.test(trimmed);
|
|
1952
|
-
}
|
|
1953
|
-
|
|
1954
|
-
/**
|
|
1955
|
-
* Check if an attribute should be removed based on default value
|
|
1956
|
-
* @param {string} tag - Element tag name (e.g., `svg`, `rect`, `path`)
|
|
1957
|
-
* @param {string} name - Attribute name
|
|
1958
|
-
* @param {string} value - Attribute value
|
|
1959
|
-
* @returns {boolean} True if attribute can be removed
|
|
1960
|
-
*/
|
|
1961
|
-
function isDefaultAttribute(tag, name, value) {
|
|
1962
|
-
// Special case: `overflow="visible"` is unsafe for root `<svg>` element
|
|
1963
|
-
// Root SVG may need explicit `overflow="visible"` to show clipped content
|
|
1964
|
-
if (name === 'overflow' && value === 'visible') {
|
|
1965
|
-
return tag !== 'svg'; // Only remove for non-root SVG elements
|
|
1966
|
-
}
|
|
1967
|
-
|
|
1968
|
-
const checker = SVG_DEFAULT_ATTRS[name];
|
|
1969
|
-
if (!checker) return false;
|
|
1970
|
-
|
|
1971
|
-
// Special case: Don’t remove `fill="black"` if stroke exists without fill
|
|
1972
|
-
// This would change the rendering (stroke-only shapes would gain black fill)
|
|
1973
|
-
if (name === 'fill' && checker(value)) {
|
|
1974
|
-
// This check would require looking at other attributes on the same element
|
|
1975
|
-
// For safety, we’ll keep this conservative and not remove `fill="black"`
|
|
1976
|
-
// in the initial implementation. Can be refined later.
|
|
1977
|
-
return false;
|
|
1978
|
-
}
|
|
1979
|
-
|
|
1980
|
-
return checker(value);
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
/**
|
|
1984
|
-
* Minify SVG attribute value based on attribute name
|
|
1985
|
-
* @param {string} name - Attribute name
|
|
1986
|
-
* @param {string} value - Attribute value
|
|
1987
|
-
* @param {Object} options - Minification options
|
|
1988
|
-
* @returns {string} Minified attribute value
|
|
1989
|
-
*/
|
|
1990
|
-
function minifySVGAttributeValue(name, value, options = {}) {
|
|
1991
|
-
if (!value || typeof value !== 'string') return value;
|
|
1992
|
-
|
|
1993
|
-
const { precision = 3, minifyColors = true } = options;
|
|
1994
|
-
|
|
1995
|
-
// Path data gets special treatment
|
|
1996
|
-
if (name === 'd') {
|
|
1997
|
-
return minifyPathData(value, precision);
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
// Numeric attributes get precision reduction and whitespace minification
|
|
2001
|
-
if (NUMERIC_ATTRS.has(name)) {
|
|
2002
|
-
const minified = value.replace(RE_NUMERIC_VALUE, (match) => {
|
|
2003
|
-
return minifyNumber(match, precision);
|
|
2004
|
-
});
|
|
2005
|
-
return minifyAttributeWhitespace(minified);
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
|
-
// Color attributes get color minification
|
|
2009
|
-
if (minifyColors && COLOR_ATTRS.has(name)) {
|
|
2010
|
-
return minifyColor(value);
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
|
-
return value;
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
|
-
/**
|
|
2017
|
-
* Check if an SVG attribute can be removed
|
|
2018
|
-
* @param {string} tag - Element tag name (e.g., `svg`, `rect`, `path`)
|
|
2019
|
-
* @param {string} name - Attribute name
|
|
2020
|
-
* @param {string} value - Attribute value
|
|
2021
|
-
* @param {Object} options - Minification options
|
|
2022
|
-
* @returns {boolean} True if attribute should be removed
|
|
2023
|
-
*/
|
|
2024
|
-
function shouldRemoveSVGAttribute(tag, name, value, options = {}) {
|
|
2025
|
-
const { removeDefaults = true } = options;
|
|
2026
|
-
|
|
2027
|
-
if (!removeDefaults) return false;
|
|
2028
|
-
|
|
2029
|
-
// Check for identity transforms
|
|
2030
|
-
if (name === 'transform' && isIdentityTransform(value)) {
|
|
2031
|
-
return true;
|
|
2032
|
-
}
|
|
2033
|
-
|
|
2034
|
-
return isDefaultAttribute(tag, name, value);
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
/**
|
|
2038
|
-
* Get default SVG minification options
|
|
2039
|
-
* @param {Object} userOptions - User-provided options
|
|
2040
|
-
* @returns {Object} Complete options object with defaults
|
|
2041
|
-
*/
|
|
2042
|
-
function getSVGMinifierOptions(userOptions) {
|
|
2043
|
-
if (typeof userOptions === 'boolean') {
|
|
2044
|
-
return userOptions ? {
|
|
2045
|
-
precision: 3,
|
|
2046
|
-
removeDefaults: true,
|
|
2047
|
-
minifyColors: true
|
|
2048
|
-
} : null;
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
if (typeof userOptions === 'object' && userOptions !== null) {
|
|
2052
|
-
return {
|
|
2053
|
-
precision: userOptions.precision ?? 3,
|
|
2054
|
-
removeDefaults: userOptions.removeDefaults ?? true,
|
|
2055
|
-
minifyColors: userOptions.minifyColors ?? true
|
|
2056
|
-
};
|
|
2057
|
-
}
|
|
2058
|
-
|
|
2059
|
-
return null;
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
1640
|
// Single source of truth for minifier option names, descriptions, types, and shared defaults
|
|
2063
1641
|
|
|
2064
1642
|
|
|
@@ -2100,11 +1678,13 @@ function shouldMinifyInnerHTML(options) {
|
|
|
2100
1678
|
* @param {Function} deps.getLightningCSS - Function to lazily load Lightning CSS
|
|
2101
1679
|
* @param {Function} deps.getTerser - Function to lazily load Terser
|
|
2102
1680
|
* @param {Function} deps.getSwc - Function to lazily load @swc/core
|
|
1681
|
+
* @param {Function} deps.getSvgo - Function to lazily load SVGO
|
|
2103
1682
|
* @param {LRU} deps.cssMinifyCache - CSS minification cache
|
|
2104
1683
|
* @param {LRU} deps.jsMinifyCache - JS minification cache
|
|
1684
|
+
* @param {LRU} deps.svgMinifyCache - SVG minification cache
|
|
2105
1685
|
* @returns {MinifierOptions} Normalized options with defaults applied
|
|
2106
1686
|
*/
|
|
2107
|
-
const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssMinifyCache, jsMinifyCache } = {}) => {
|
|
1687
|
+
const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getSvgo, cssMinifyCache, jsMinifyCache, svgMinifyCache } = {}) => {
|
|
2108
1688
|
const options = {
|
|
2109
1689
|
name: lowercase,
|
|
2110
1690
|
canCollapseWhitespace,
|
|
@@ -2405,11 +1985,54 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
2405
1985
|
return text;
|
|
2406
1986
|
}
|
|
2407
1987
|
};
|
|
2408
|
-
} else if (key === 'minifySVG') {
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
1988
|
+
} else if (key === 'minifySVG' && typeof option !== 'function') {
|
|
1989
|
+
if (!option) {
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
const svgoOptions = typeof option === 'object' ? option : {};
|
|
1994
|
+
|
|
1995
|
+
// Pre-compute option signature for cache keys
|
|
1996
|
+
const svgSig = stableStringify({
|
|
1997
|
+
...svgoOptions,
|
|
1998
|
+
cont: !!options.continueOnMinifyError
|
|
1999
|
+
});
|
|
2000
|
+
|
|
2001
|
+
options.minifySVG = async function (svgContent) {
|
|
2002
|
+
if (!svgContent || !svgContent.trim()) {
|
|
2003
|
+
return svgContent;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
// Cache key
|
|
2007
|
+
const svgKey = svgContent.length > 2048
|
|
2008
|
+
? (svgContent.length + '|' + svgContent.slice(0, 50) + svgContent.slice(-50) + '|' + svgSig)
|
|
2009
|
+
: (svgContent + '|' + svgSig);
|
|
2010
|
+
|
|
2011
|
+
try {
|
|
2012
|
+
const cached = svgMinifyCache.get(svgKey);
|
|
2013
|
+
if (cached) {
|
|
2014
|
+
return await cached;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
const inFlight = (async () => {
|
|
2018
|
+
const optimize = await getSvgo();
|
|
2019
|
+
const result = optimize(svgContent, svgoOptions);
|
|
2020
|
+
return result.data;
|
|
2021
|
+
})();
|
|
2022
|
+
|
|
2023
|
+
svgMinifyCache.set(svgKey, inFlight);
|
|
2024
|
+
const resolved = await inFlight;
|
|
2025
|
+
svgMinifyCache.set(svgKey, resolved);
|
|
2026
|
+
return resolved;
|
|
2027
|
+
} catch (err) {
|
|
2028
|
+
svgMinifyCache.delete(svgKey);
|
|
2029
|
+
if (!options.continueOnMinifyError) {
|
|
2030
|
+
throw err;
|
|
2031
|
+
}
|
|
2032
|
+
options.log && options.log(err);
|
|
2033
|
+
return svgContent;
|
|
2034
|
+
}
|
|
2035
|
+
};
|
|
2413
2036
|
} else if (key === 'customAttrCollapse') {
|
|
2414
2037
|
// Single regex pattern
|
|
2415
2038
|
options[key] = parseRegExp(option);
|
|
@@ -2813,17 +2436,6 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
2813
2436
|
return attrValue;
|
|
2814
2437
|
}
|
|
2815
2438
|
return minifyHTMLSelf(attrValue, options, true);
|
|
2816
|
-
} else if (options.insideSVG && options.minifySVG) {
|
|
2817
|
-
// Apply SVG-specific attribute minification when inside SVG elements
|
|
2818
|
-
try {
|
|
2819
|
-
return minifySVGAttributeValue(attrName, attrValue, options.minifySVG);
|
|
2820
|
-
} catch (err) {
|
|
2821
|
-
if (!options.continueOnMinifyError) {
|
|
2822
|
-
throw err;
|
|
2823
|
-
}
|
|
2824
|
-
options.log && options.log(err);
|
|
2825
|
-
return attrValue;
|
|
2826
|
-
}
|
|
2827
2439
|
}
|
|
2828
2440
|
return attrValue;
|
|
2829
2441
|
}
|
|
@@ -2864,9 +2476,7 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
2864
2476
|
(options.removeScriptTypeAttributes && tag === 'script' &&
|
|
2865
2477
|
attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue)) ||
|
|
2866
2478
|
(options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
|
|
2867
|
-
attrName === 'type' && isStyleLinkTypeAttribute(attrValue))
|
|
2868
|
-
(options.insideSVG && options.minifySVG &&
|
|
2869
|
-
shouldRemoveSVGAttribute(tag, attrName, attrValue, options.minifySVG))) {
|
|
2479
|
+
attrName === 'type' && isStyleLinkTypeAttribute(attrValue))) {
|
|
2870
2480
|
return;
|
|
2871
2481
|
}
|
|
2872
2482
|
|
|
@@ -3273,9 +2883,18 @@ async function getSwc() {
|
|
|
3273
2883
|
return swcPromise;
|
|
3274
2884
|
}
|
|
3275
2885
|
|
|
2886
|
+
let svgoPromise;
|
|
2887
|
+
async function getSvgo() {
|
|
2888
|
+
if (!svgoPromise) {
|
|
2889
|
+
svgoPromise = import('svgo').then(m => m.optimize);
|
|
2890
|
+
}
|
|
2891
|
+
return svgoPromise;
|
|
2892
|
+
}
|
|
2893
|
+
|
|
3276
2894
|
// Minification caches (initialized on first use with configurable sizes)
|
|
3277
2895
|
let cssMinifyCache = null;
|
|
3278
2896
|
let jsMinifyCache = null;
|
|
2897
|
+
let svgMinifyCache = null;
|
|
3279
2898
|
|
|
3280
2899
|
// Pre-compiled patterns for script merging (avoid repeated allocation in hot path)
|
|
3281
2900
|
const RE_SCRIPT_ATTRS = /([^\s=]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
@@ -3445,6 +3064,15 @@ function mergeConsecutiveScripts(html) {
|
|
|
3445
3064
|
*
|
|
3446
3065
|
* Default: `500`
|
|
3447
3066
|
*
|
|
3067
|
+
* @prop {number} [cacheSVG]
|
|
3068
|
+
* The maximum number of entries for the SVG minification cache. Higher
|
|
3069
|
+
* values improve performance for inputs with repeated SVG content.
|
|
3070
|
+
* - Cache is created on first `minify()` call and persists for the process lifetime
|
|
3071
|
+
* - Cache size is locked after first call—subsequent calls reuse the same cache
|
|
3072
|
+
* - Explicit `0` values are coerced to `1` (minimum functional cache size)
|
|
3073
|
+
*
|
|
3074
|
+
* Default: `500`
|
|
3075
|
+
*
|
|
3448
3076
|
* @prop {boolean} [caseSensitive]
|
|
3449
3077
|
* When true, tag and attribute names are treated as case-sensitive.
|
|
3450
3078
|
* Useful for custom HTML tags.
|
|
@@ -3627,12 +3255,10 @@ function mergeConsecutiveScripts(html) {
|
|
|
3627
3255
|
*
|
|
3628
3256
|
* Default: `false`
|
|
3629
3257
|
*
|
|
3630
|
-
* @prop {boolean |
|
|
3631
|
-
* When true, enables SVG
|
|
3632
|
-
*
|
|
3633
|
-
*
|
|
3634
|
-
* - `removeDefaults`: Remove attributes with default values (e.g., `fill="black"`). Default: `true`
|
|
3635
|
-
* - `minifyColors`: Minify color values (hex shortening, rgb to hex conversion). Default: `true`
|
|
3258
|
+
* @prop {boolean | Object} [minifySVG]
|
|
3259
|
+
* When true, enables SVG minification using [SVGO](https://github.com/svg/svgo).
|
|
3260
|
+
* Complete SVG subtrees are extracted and optimized as a block.
|
|
3261
|
+
* If an object is provided, it is passed to SVGO as configuration options.
|
|
3636
3262
|
* If disabled, SVG content is minified using standard HTML rules only.
|
|
3637
3263
|
*
|
|
3638
3264
|
* Default: `false`
|
|
@@ -4216,6 +3842,11 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4216
3842
|
trimTrailingWhitespace(charsIndex, nextTag);
|
|
4217
3843
|
}
|
|
4218
3844
|
|
|
3845
|
+
// SVG subtree capture: When SVGO is active, record buffer positions for post-processing
|
|
3846
|
+
const svgBlocks = []; // Array of { start, end } buffer indices
|
|
3847
|
+
let svgBufferStartIndex = -1;
|
|
3848
|
+
let svgDepth = 0;
|
|
3849
|
+
|
|
4219
3850
|
const parser = new HTMLParser(value, {
|
|
4220
3851
|
partialMarkup: partialMarkup ?? options.partialMarkup,
|
|
4221
3852
|
continueOnParseError: options.continueOnParseError,
|
|
@@ -4233,6 +3864,10 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4233
3864
|
options.name = identity;
|
|
4234
3865
|
options.insideSVG = lowerTag === 'svg';
|
|
4235
3866
|
options.insideForeignContent = true;
|
|
3867
|
+
// Disable HTML-specific options that produce invalid XML
|
|
3868
|
+
options.removeAttributeQuotes = false;
|
|
3869
|
+
options.removeTagWhitespace = false;
|
|
3870
|
+
options.decodeEntities = false;
|
|
4236
3871
|
}
|
|
4237
3872
|
// `foreignObject` in SVG and `annotation-xml` in MathML contain HTML content
|
|
4238
3873
|
// Note: The element itself is in SVG/MathML namespace, only its children are HTML
|
|
@@ -4247,6 +3882,9 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4247
3882
|
options.parentName = parentName; // Preserve for the element tag itself
|
|
4248
3883
|
options.name = options.htmlName || lowercase;
|
|
4249
3884
|
options.insideForeignContent = false;
|
|
3885
|
+
// Note: `removeAttributeQuotes`, `removeTagWhitespace`, and `decodeEntities`
|
|
3886
|
+
// stay disabled (inherited from SVG context) because the entire SVG block
|
|
3887
|
+
// must be valid XML for SVGO processing
|
|
4250
3888
|
useParentNameForTag = true;
|
|
4251
3889
|
}
|
|
4252
3890
|
tag = (useParentNameForTag ? options.parentName : options.name)(tag);
|
|
@@ -4298,6 +3936,14 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4298
3936
|
}
|
|
4299
3937
|
}
|
|
4300
3938
|
|
|
3939
|
+
// Track SVG subtree for SVGO block processing
|
|
3940
|
+
if (lowerTag === 'svg' && options.minifySVG) {
|
|
3941
|
+
if (svgDepth === 0) {
|
|
3942
|
+
svgBufferStartIndex = buffer.length; // Record position before <svg> is pushed
|
|
3943
|
+
}
|
|
3944
|
+
svgDepth++;
|
|
3945
|
+
}
|
|
3946
|
+
|
|
4301
3947
|
const openTag = '<' + tag;
|
|
4302
3948
|
const hasUnarySlash = unarySlash && options.keepClosingSlash;
|
|
4303
3949
|
|
|
@@ -4428,6 +4074,15 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4428
4074
|
currentChars += '|';
|
|
4429
4075
|
}
|
|
4430
4076
|
}
|
|
4077
|
+
|
|
4078
|
+
// SVG subtree capture: Record end position for post-processing with SVGO
|
|
4079
|
+
if (lowerTag === 'svg' && options.minifySVG && svgDepth > 0) {
|
|
4080
|
+
svgDepth--;
|
|
4081
|
+
if (svgDepth === 0 && svgBufferStartIndex >= 0) {
|
|
4082
|
+
svgBlocks.push({ start: svgBufferStartIndex, end: buffer.length });
|
|
4083
|
+
svgBufferStartIndex = -1;
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4431
4086
|
},
|
|
4432
4087
|
chars: async function (text, prevTag, nextTag, prevAttrs, nextAttrs) {
|
|
4433
4088
|
prevTag = prevTag === '' ? 'comment' : prevTag;
|
|
@@ -4652,6 +4307,19 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4652
4307
|
|
|
4653
4308
|
await parser.parse();
|
|
4654
4309
|
|
|
4310
|
+
// Post-processing: Optimize SVG blocks with SVGO
|
|
4311
|
+
// Run all SVGO calls in parallel, then splice results in reverse to preserve indices
|
|
4312
|
+
if (options.minifySVG && svgBlocks.length) {
|
|
4313
|
+
const optimized = await Promise.all(
|
|
4314
|
+
svgBlocks.map(({ start, end }) =>
|
|
4315
|
+
options.minifySVG(buffer.slice(start, end).join(''))
|
|
4316
|
+
)
|
|
4317
|
+
);
|
|
4318
|
+
for (let i = svgBlocks.length - 1; i >= 0; i--) {
|
|
4319
|
+
buffer.splice(svgBlocks[i].start, svgBlocks[i].end - svgBlocks[i].start, optimized[i]);
|
|
4320
|
+
}
|
|
4321
|
+
}
|
|
4322
|
+
|
|
4655
4323
|
if (options.removeOptionalTags) {
|
|
4656
4324
|
// `<html>` may be omitted if first thing inside is not a comment
|
|
4657
4325
|
// `<head>` or `<body>` may be omitted if empty
|
|
@@ -4738,7 +4406,7 @@ function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
|
|
|
4738
4406
|
* Important behavior notes:
|
|
4739
4407
|
* - Caches are created on the first `minify()` call and persist for the lifetime of the process
|
|
4740
4408
|
* - Cache sizes are locked after first initialization—subsequent calls use the same caches
|
|
4741
|
-
* even if different `cacheCSS`/`cacheJS` options are provided
|
|
4409
|
+
* even if different `cacheCSS`/`cacheJS`/`cacheSVG` options are provided
|
|
4742
4410
|
* - The first call’s options determine the cache sizes for subsequent calls
|
|
4743
4411
|
* - Explicit `0` values are coerced to `1` (minimum functional cache size)
|
|
4744
4412
|
*/
|
|
@@ -4762,16 +4430,20 @@ function initCaches(options) {
|
|
|
4762
4430
|
: (parseEnvCacheSize(process.env.HMN_CACHE_CSS) ?? defaultSize);
|
|
4763
4431
|
const jsSize = options.cacheJS !== undefined ? options.cacheJS
|
|
4764
4432
|
: (parseEnvCacheSize(process.env.HMN_CACHE_JS) ?? defaultSize);
|
|
4433
|
+
const svgSize = options.cacheSVG !== undefined ? options.cacheSVG
|
|
4434
|
+
: (parseEnvCacheSize(process.env.HMN_CACHE_SVG) ?? defaultSize);
|
|
4765
4435
|
|
|
4766
4436
|
// Coerce `0` to `1` (minimum functional cache size) to avoid immediate eviction
|
|
4767
4437
|
const cssFinalSize = cssSize === 0 ? 1 : cssSize;
|
|
4768
4438
|
const jsFinalSize = jsSize === 0 ? 1 : jsSize;
|
|
4439
|
+
const svgFinalSize = svgSize === 0 ? 1 : svgSize;
|
|
4769
4440
|
|
|
4770
4441
|
cssMinifyCache = new LRU(cssFinalSize);
|
|
4771
4442
|
jsMinifyCache = new LRU(jsFinalSize);
|
|
4443
|
+
svgMinifyCache = new LRU(svgFinalSize);
|
|
4772
4444
|
}
|
|
4773
4445
|
|
|
4774
|
-
return { cssMinifyCache, jsMinifyCache };
|
|
4446
|
+
return { cssMinifyCache, jsMinifyCache, svgMinifyCache };
|
|
4775
4447
|
}
|
|
4776
4448
|
|
|
4777
4449
|
/**
|
|
@@ -4789,6 +4461,7 @@ const minify = async function (value, options) {
|
|
|
4789
4461
|
getLightningCSS,
|
|
4790
4462
|
getTerser,
|
|
4791
4463
|
getSwc,
|
|
4464
|
+
getSvgo,
|
|
4792
4465
|
...caches
|
|
4793
4466
|
});
|
|
4794
4467
|
let result = await minifyHTML(value, options);
|