html-minifier-next 4.14.1 → 4.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -31
- package/cli.js +3 -2
- package/dist/htmlminifier.cjs +323 -4
- package/dist/htmlminifier.esm.bundle.js +323 -4
- package/dist/types/htmlminifier.d.ts +15 -0
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/lib/attributes.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts.map +1 -1
- package/dist/types/lib/svg.d.ts +23 -0
- package/dist/types/lib/svg.d.ts.map +1 -0
- package/dist/types/lib/whitespace.d.ts.map +1 -1
- package/dist/types/presets.d.ts +1 -0
- package/dist/types/presets.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/htmlminifier.js +11 -0
- package/src/htmlparser.js +1 -1
- package/src/lib/attributes.js +15 -1
- package/src/lib/options.js +10 -2
- package/src/lib/svg.js +272 -0
- package/src/lib/whitespace.js +16 -1
- package/src/presets.js +1 -0
package/src/lib/svg.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight SVG optimizations:
|
|
3
|
+
*
|
|
4
|
+
* - Numeric precision reduction for coordinates and path data
|
|
5
|
+
* - Whitespace removal in attribute values (numeric sequences)
|
|
6
|
+
* - Default attribute removal (safe, well-documented defaults)
|
|
7
|
+
* - Color minification (hex shortening, rgb() to hex)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default SVG attribute values that can be safely removed
|
|
12
|
+
* Only includes well-documented, widely-supported defaults
|
|
13
|
+
*/
|
|
14
|
+
const SVG_DEFAULT_ATTRS = {
|
|
15
|
+
// Fill and stroke defaults
|
|
16
|
+
fill: value => value === 'black' || value === '#000' || value === '#000000',
|
|
17
|
+
'fill-opacity': value => value === '1',
|
|
18
|
+
'fill-rule': value => value === 'nonzero',
|
|
19
|
+
stroke: value => value === 'none',
|
|
20
|
+
'stroke-dasharray': value => value === 'none',
|
|
21
|
+
'stroke-dashoffset': value => value === '0',
|
|
22
|
+
'stroke-linecap': value => value === 'butt',
|
|
23
|
+
'stroke-linejoin': value => value === 'miter',
|
|
24
|
+
'stroke-miterlimit': value => value === '4',
|
|
25
|
+
'stroke-opacity': value => value === '1',
|
|
26
|
+
'stroke-width': value => value === '1',
|
|
27
|
+
|
|
28
|
+
// Text and font defaults
|
|
29
|
+
'font-family': value => value === 'inherit',
|
|
30
|
+
'font-size': value => value === 'medium',
|
|
31
|
+
'font-style': value => value === 'normal',
|
|
32
|
+
'font-variant': value => value === 'normal',
|
|
33
|
+
'font-weight': value => value === 'normal',
|
|
34
|
+
'letter-spacing': value => value === 'normal',
|
|
35
|
+
'text-decoration': value => value === 'none',
|
|
36
|
+
'text-anchor': value => value === 'start',
|
|
37
|
+
|
|
38
|
+
// Other common defaults
|
|
39
|
+
opacity: value => value === '1',
|
|
40
|
+
visibility: value => value === 'visible',
|
|
41
|
+
display: value => value === 'inline',
|
|
42
|
+
overflow: value => value === 'visible'
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Minify numeric value by removing trailing zeros and unnecessary decimals
|
|
47
|
+
* @param {string} num - Numeric string to minify
|
|
48
|
+
* @param {number} precision - Maximum decimal places to keep
|
|
49
|
+
* @returns {string} Minified numeric string
|
|
50
|
+
*/
|
|
51
|
+
function minifyNumber(num, precision = 3) {
|
|
52
|
+
const parsed = parseFloat(num);
|
|
53
|
+
|
|
54
|
+
// Handle special cases
|
|
55
|
+
if (isNaN(parsed)) return num;
|
|
56
|
+
if (parsed === 0) return '0';
|
|
57
|
+
if (!isFinite(parsed)) return num;
|
|
58
|
+
|
|
59
|
+
// Convert to fixed precision, then remove trailing zeros
|
|
60
|
+
const fixed = parsed.toFixed(precision);
|
|
61
|
+
const trimmed = fixed.replace(/\.?0+$/, '');
|
|
62
|
+
|
|
63
|
+
return trimmed || '0';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Minify SVG path data by reducing numeric precision
|
|
68
|
+
* @param {string} pathData - SVG path data string
|
|
69
|
+
* @param {number} precision - Decimal precision for coordinates
|
|
70
|
+
* @returns {string} Minified path data
|
|
71
|
+
*/
|
|
72
|
+
function minifyPathData(pathData, precision = 3) {
|
|
73
|
+
if (!pathData || typeof pathData !== 'string') return pathData;
|
|
74
|
+
|
|
75
|
+
// Match numbers (including scientific notation and negative values)
|
|
76
|
+
// Regex: optional minus, digits, optional decimal point and more digits, optional exponent
|
|
77
|
+
return pathData.replace(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g, (match) => {
|
|
78
|
+
return minifyNumber(match, precision);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Minify whitespace in numeric attribute values
|
|
84
|
+
* Examples:
|
|
85
|
+
* "10 , 20" → "10,20"
|
|
86
|
+
* "translate( 10 20 )" → "translate(10 20)"
|
|
87
|
+
* "100, 10 40, 198" → "100,10 40,198"
|
|
88
|
+
*
|
|
89
|
+
* @param {string} value - Attribute value to minify
|
|
90
|
+
* @returns {string} Minified value
|
|
91
|
+
*/
|
|
92
|
+
function minifyAttributeWhitespace(value) {
|
|
93
|
+
if (!value || typeof value !== 'string') return value;
|
|
94
|
+
|
|
95
|
+
return value
|
|
96
|
+
// Remove spaces around commas
|
|
97
|
+
.replace(/\s*,\s*/g, ',')
|
|
98
|
+
// Remove spaces around parentheses
|
|
99
|
+
.replace(/\(\s+/g, '(')
|
|
100
|
+
.replace(/\s+\)/g, ')')
|
|
101
|
+
// Collapse multiple spaces to single space
|
|
102
|
+
.replace(/\s+/g, ' ')
|
|
103
|
+
// Trim leading/trailing whitespace
|
|
104
|
+
.trim();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Minify color values (hex shortening, rgb to hex conversion)
|
|
109
|
+
* @param {string} color - Color value to minify
|
|
110
|
+
* @returns {string} Minified color value
|
|
111
|
+
*/
|
|
112
|
+
function minifyColor(color) {
|
|
113
|
+
if (!color || typeof color !== 'string') return color;
|
|
114
|
+
|
|
115
|
+
const trimmed = color.trim().toLowerCase();
|
|
116
|
+
|
|
117
|
+
// Shorten 6-digit hex to 3-digit when possible
|
|
118
|
+
// #aabbcc → #abc, #000000 → #000
|
|
119
|
+
const hexMatch = trimmed.match(/^#([0-9a-f]{6})$/);
|
|
120
|
+
if (hexMatch) {
|
|
121
|
+
const hex = hexMatch[1];
|
|
122
|
+
if (hex[0] === hex[1] && hex[2] === hex[3] && hex[4] === hex[5]) {
|
|
123
|
+
return '#' + hex[0] + hex[2] + hex[4];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Convert rgb(255,255,255) to hex
|
|
128
|
+
const rgbMatch = trimmed.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
|
|
129
|
+
if (rgbMatch) {
|
|
130
|
+
const r = parseInt(rgbMatch[1], 10);
|
|
131
|
+
const g = parseInt(rgbMatch[2], 10);
|
|
132
|
+
const b = parseInt(rgbMatch[3], 10);
|
|
133
|
+
|
|
134
|
+
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
|
|
135
|
+
const toHex = (n) => {
|
|
136
|
+
const h = n.toString(16);
|
|
137
|
+
return h.length === 1 ? '0' + h : h;
|
|
138
|
+
};
|
|
139
|
+
const hexColor = '#' + toHex(r) + toHex(g) + toHex(b);
|
|
140
|
+
|
|
141
|
+
// Try to shorten if possible
|
|
142
|
+
if (hexColor[1] === hexColor[2] && hexColor[3] === hexColor[4] && hexColor[5] === hexColor[6]) {
|
|
143
|
+
return '#' + hexColor[1] + hexColor[3] + hexColor[5];
|
|
144
|
+
}
|
|
145
|
+
return hexColor;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return color;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Attributes that contain numeric sequences or path data
|
|
153
|
+
const NUMERIC_ATTRS = new Set([
|
|
154
|
+
'd', // Path data
|
|
155
|
+
'points', // Polygon/polyline points
|
|
156
|
+
'viewBox', // viewBox coordinates
|
|
157
|
+
'transform', // Transform functions
|
|
158
|
+
'x', 'y', 'x1', 'y1', 'x2', 'y2', // Coordinates
|
|
159
|
+
'cx', 'cy', 'r', 'rx', 'ry', // Circle/ellipse
|
|
160
|
+
'width', 'height', // Dimensions
|
|
161
|
+
'dx', 'dy', // Text offsets
|
|
162
|
+
'offset', // Gradient offset
|
|
163
|
+
'startOffset', // textPath
|
|
164
|
+
'pathLength', // Path length
|
|
165
|
+
'stdDeviation', // Filter params
|
|
166
|
+
'baseFrequency', // Turbulence
|
|
167
|
+
'k1', 'k2', 'k3', 'k4' // Composite filter
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
// Attributes that contain color values
|
|
171
|
+
const COLOR_ATTRS = new Set([
|
|
172
|
+
'fill',
|
|
173
|
+
'stroke',
|
|
174
|
+
'stop-color',
|
|
175
|
+
'flood-color',
|
|
176
|
+
'lighting-color'
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Check if an attribute should be removed based on default value
|
|
181
|
+
* @param {string} name - Attribute name
|
|
182
|
+
* @param {string} value - Attribute value
|
|
183
|
+
* @returns {boolean} True if attribute can be removed
|
|
184
|
+
*/
|
|
185
|
+
function isDefaultAttribute(name, value) {
|
|
186
|
+
const checker = SVG_DEFAULT_ATTRS[name];
|
|
187
|
+
if (!checker) return false;
|
|
188
|
+
|
|
189
|
+
// Special case: Don’t remove `fill="black"` if stroke exists without fill
|
|
190
|
+
// This would change the rendering (stroke-only shapes would gain black fill)
|
|
191
|
+
if (name === 'fill' && checker(value)) {
|
|
192
|
+
// This check would require looking at other attributes on the same element
|
|
193
|
+
// For safety, we’ll keep this conservative and not remove `fill="black"`
|
|
194
|
+
// in the initial implementation. Can be refined later.
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return checker(value);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Minify SVG attribute value based on attribute name
|
|
203
|
+
* @param {string} name - Attribute name
|
|
204
|
+
* @param {string} value - Attribute value
|
|
205
|
+
* @param {Object} options - Minification options
|
|
206
|
+
* @returns {string} Minified attribute value
|
|
207
|
+
*/
|
|
208
|
+
export function minifySVGAttributeValue(name, value, options = {}) {
|
|
209
|
+
if (!value || typeof value !== 'string') return value;
|
|
210
|
+
|
|
211
|
+
const { precision = 3, minifyColors = true } = options;
|
|
212
|
+
|
|
213
|
+
// Path data gets special treatment
|
|
214
|
+
if (name === 'd') {
|
|
215
|
+
return minifyPathData(value, precision);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Numeric attributes get precision reduction and whitespace minification
|
|
219
|
+
if (NUMERIC_ATTRS.has(name)) {
|
|
220
|
+
const minified = value.replace(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g, (match) => {
|
|
221
|
+
return minifyNumber(match, precision);
|
|
222
|
+
});
|
|
223
|
+
return minifyAttributeWhitespace(minified);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Color attributes get color minification
|
|
227
|
+
if (minifyColors && COLOR_ATTRS.has(name)) {
|
|
228
|
+
return minifyColor(value);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return value;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Check if an SVG attribute can be removed
|
|
236
|
+
* @param {string} name - Attribute name
|
|
237
|
+
* @param {string} value - Attribute value
|
|
238
|
+
* @param {Object} options - Minification options
|
|
239
|
+
* @returns {boolean} True if attribute should be removed
|
|
240
|
+
*/
|
|
241
|
+
export function shouldRemoveSVGAttribute(name, value, options = {}) {
|
|
242
|
+
const { removeDefaults = true } = options;
|
|
243
|
+
|
|
244
|
+
if (!removeDefaults) return false;
|
|
245
|
+
|
|
246
|
+
return isDefaultAttribute(name, value);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get default SVG minification options
|
|
251
|
+
* @param {Object} userOptions - User-provided options
|
|
252
|
+
* @returns {Object} Complete options object with defaults
|
|
253
|
+
*/
|
|
254
|
+
export function getSVGMinifierOptions(userOptions) {
|
|
255
|
+
if (typeof userOptions === 'boolean') {
|
|
256
|
+
return userOptions ? {
|
|
257
|
+
precision: 3,
|
|
258
|
+
removeDefaults: true,
|
|
259
|
+
minifyColors: true
|
|
260
|
+
} : null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (typeof userOptions === 'object' && userOptions !== null) {
|
|
264
|
+
return {
|
|
265
|
+
precision: userOptions.precision ?? 3,
|
|
266
|
+
removeDefaults: userOptions.removeDefaults ?? true,
|
|
267
|
+
minifyColors: userOptions.minifyColors ?? true
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return null;
|
|
272
|
+
}
|
package/src/lib/whitespace.js
CHANGED
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
RE_NBSP_LEAD_GROUP,
|
|
9
9
|
RE_NBSP_TRAILING_GROUP,
|
|
10
10
|
RE_NBSP_TRAILING_STRIP,
|
|
11
|
-
inlineElementsToKeepWhitespace
|
|
11
|
+
inlineElementsToKeepWhitespace,
|
|
12
|
+
inlineElementsToKeepWhitespaceWithin
|
|
12
13
|
} from './constants.js';
|
|
13
14
|
|
|
14
15
|
// Trim whitespace
|
|
@@ -110,10 +111,24 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements,
|
|
|
110
111
|
if (trimLeft && !options.collapseInlineTagWhitespace) {
|
|
111
112
|
trimLeft = prevTag.charAt(0) === '/' ? !inlineElements.has(prevTag.slice(1)) : !inlineTextSet.has(prevTag);
|
|
112
113
|
}
|
|
114
|
+
// When `collapseInlineTagWhitespace` is enabled, still preserve whitespace around inline text elements
|
|
115
|
+
if (trimLeft && options.collapseInlineTagWhitespace) {
|
|
116
|
+
const tagName = prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag;
|
|
117
|
+
if (inlineElementsToKeepWhitespaceWithin.has(tagName)) {
|
|
118
|
+
trimLeft = false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
113
121
|
let trimRight = nextTag && !inlineElementsToKeepWhitespace.has(nextTag);
|
|
114
122
|
if (trimRight && !options.collapseInlineTagWhitespace) {
|
|
115
123
|
trimRight = nextTag.charAt(0) === '/' ? !inlineTextSet.has(nextTag.slice(1)) : !inlineElements.has(nextTag);
|
|
116
124
|
}
|
|
125
|
+
// When `collapseInlineTagWhitespace` is enabled, still preserve whitespace around inline text elements
|
|
126
|
+
if (trimRight && options.collapseInlineTagWhitespace) {
|
|
127
|
+
const tagName = nextTag.charAt(0) === '/' ? nextTag.slice(1) : nextTag;
|
|
128
|
+
if (inlineElementsToKeepWhitespaceWithin.has(tagName)) {
|
|
129
|
+
trimRight = false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
117
132
|
return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
|
|
118
133
|
}
|
|
119
134
|
|