html-minifier-next 4.16.4 → 4.17.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 +57 -70
- package/cli.js +29 -26
- package/dist/htmlminifier.cjs +280 -139
- package/dist/htmlminifier.esm.bundle.js +280 -139
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/htmlparser.d.ts.map +1 -1
- package/dist/types/lib/attributes.d.ts +1 -1
- package/dist/types/lib/attributes.d.ts.map +1 -1
- package/dist/types/lib/constants.d.ts +16 -15
- package/dist/types/lib/constants.d.ts.map +1 -1
- package/dist/types/lib/content.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts +2 -2
- package/dist/types/lib/options.d.ts.map +1 -1
- package/dist/types/lib/whitespace.d.ts +1 -1
- package/dist/types/lib/whitespace.d.ts.map +1 -1
- package/dist/types/presets.d.ts +1 -2
- package/dist/types/presets.d.ts.map +1 -1
- package/package.json +9 -8
- package/src/htmlminifier.js +49 -47
- package/src/htmlparser.js +44 -13
- package/src/lib/attributes.js +72 -30
- package/src/lib/constants.js +46 -39
- package/src/lib/content.js +0 -1
- package/src/lib/elements.js +15 -15
- package/src/lib/options.js +26 -9
- package/src/lib/svg.js +14 -14
- package/src/lib/whitespace.js +53 -4
- package/src/presets.js +4 -5
- package/src/tokenchain.js +2 -2
- package/src/lib/index.js +0 -20
package/src/lib/elements.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// Imports
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
headerElements,
|
|
5
|
+
descriptionElements,
|
|
6
|
+
pBlockElements,
|
|
7
7
|
rubyEndTagOmission,
|
|
8
8
|
rubyRtcEndTagOmission,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
optionElements,
|
|
10
|
+
tableContentElements,
|
|
11
|
+
tableSectionElements,
|
|
12
|
+
cellElements
|
|
13
13
|
} from './constants.js';
|
|
14
14
|
import { hasAttrName } from './attributes.js';
|
|
15
15
|
|
|
@@ -21,7 +21,7 @@ function canRemoveParentTag(optionalStartTag, tag) {
|
|
|
21
21
|
case 'head':
|
|
22
22
|
return true;
|
|
23
23
|
case 'body':
|
|
24
|
-
return !
|
|
24
|
+
return !headerElements.has(tag);
|
|
25
25
|
case 'colgroup':
|
|
26
26
|
return tag === 'col';
|
|
27
27
|
case 'tbody':
|
|
@@ -35,7 +35,7 @@ function isStartTagMandatory(optionalEndTag, tag) {
|
|
|
35
35
|
case 'colgroup':
|
|
36
36
|
return optionalEndTag === 'colgroup';
|
|
37
37
|
case 'tbody':
|
|
38
|
-
return
|
|
38
|
+
return tableSectionElements.has(optionalEndTag);
|
|
39
39
|
}
|
|
40
40
|
return false;
|
|
41
41
|
}
|
|
@@ -54,9 +54,9 @@ function canRemovePrecedingTag(optionalEndTag, tag) {
|
|
|
54
54
|
return tag === optionalEndTag;
|
|
55
55
|
case 'dt':
|
|
56
56
|
case 'dd':
|
|
57
|
-
return
|
|
57
|
+
return descriptionElements.has(tag);
|
|
58
58
|
case 'p':
|
|
59
|
-
return
|
|
59
|
+
return pBlockElements.has(tag);
|
|
60
60
|
case 'rb':
|
|
61
61
|
case 'rt':
|
|
62
62
|
case 'rp':
|
|
@@ -64,15 +64,15 @@ function canRemovePrecedingTag(optionalEndTag, tag) {
|
|
|
64
64
|
case 'rtc':
|
|
65
65
|
return rubyRtcEndTagOmission.has(tag);
|
|
66
66
|
case 'option':
|
|
67
|
-
return
|
|
67
|
+
return optionElements.has(tag);
|
|
68
68
|
case 'thead':
|
|
69
69
|
case 'tbody':
|
|
70
|
-
return
|
|
70
|
+
return tableContentElements.has(tag);
|
|
71
71
|
case 'tfoot':
|
|
72
72
|
return tag === 'tbody';
|
|
73
73
|
case 'td':
|
|
74
74
|
case 'th':
|
|
75
|
-
return
|
|
75
|
+
return cellElements.has(tag);
|
|
76
76
|
}
|
|
77
77
|
return false;
|
|
78
78
|
}
|
|
@@ -175,7 +175,7 @@ function parseRemoveEmptyElementsExcept(input, options) {
|
|
|
175
175
|
if (typeof item === 'string') {
|
|
176
176
|
const spec = parseElementSpec(item, options);
|
|
177
177
|
if (!spec && options.log) {
|
|
178
|
-
options.log('Warning: Unable to parse “removeEmptyElementsExcept” specification:
|
|
178
|
+
options.log('Warning: Unable to parse “removeEmptyElementsExcept” specification: “' + item + '”');
|
|
179
179
|
}
|
|
180
180
|
return spec;
|
|
181
181
|
}
|
package/src/lib/options.js
CHANGED
|
@@ -6,6 +6,7 @@ import { RE_TRAILING_SEMICOLON } from './constants.js';
|
|
|
6
6
|
import { canCollapseWhitespace, canTrimWhitespace } from './whitespace.js';
|
|
7
7
|
import { wrapCSS, unwrapCSS } from './content.js';
|
|
8
8
|
import { getSVGMinifierOptions } from './svg.js';
|
|
9
|
+
import { getPreset, getPresetNames } from '../presets.js';
|
|
9
10
|
|
|
10
11
|
// Helper functions
|
|
11
12
|
|
|
@@ -26,8 +27,8 @@ function shouldMinifyInnerHTML(options) {
|
|
|
26
27
|
/**
|
|
27
28
|
* @param {Partial<MinifierOptions>} inputOptions - User-provided options
|
|
28
29
|
* @param {Object} deps - Dependencies from htmlminifier.js
|
|
29
|
-
* @param {Function} deps.getLightningCSS - Function to lazily load
|
|
30
|
-
* @param {Function} deps.getTerser - Function to lazily load
|
|
30
|
+
* @param {Function} deps.getLightningCSS - Function to lazily load Lightning CSS
|
|
31
|
+
* @param {Function} deps.getTerser - Function to lazily load Terser
|
|
31
32
|
* @param {Function} deps.getSwc - Function to lazily load @swc/core
|
|
32
33
|
* @param {LRU} deps.cssMinifyCache - CSS minification cache
|
|
33
34
|
* @param {LRU} deps.jsMinifyCache - JS minification cache
|
|
@@ -64,7 +65,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
64
65
|
if (typeof value === 'string') {
|
|
65
66
|
return new RegExp(value.replace(/^\/(.*)\/$/, '$1'));
|
|
66
67
|
}
|
|
67
|
-
return value; // Already a RegExp or
|
|
68
|
+
return value; // Already a RegExp or another type
|
|
68
69
|
};
|
|
69
70
|
|
|
70
71
|
const parseRegExpArray = (arr) => {
|
|
@@ -84,9 +85,25 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
84
85
|
});
|
|
85
86
|
};
|
|
86
87
|
|
|
88
|
+
// Apply preset first if specified (so user options can override preset values)
|
|
89
|
+
if (inputOptions.preset) {
|
|
90
|
+
const preset = getPreset(inputOptions.preset);
|
|
91
|
+
if (preset) {
|
|
92
|
+
Object.assign(options, preset);
|
|
93
|
+
} else {
|
|
94
|
+
const available = getPresetNames().join(', ');
|
|
95
|
+
console.warn(`HTML Minifier Next: Unknown preset “${inputOptions.preset}”. Available presets: ${available}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
87
99
|
Object.keys(inputOptions).forEach(function (key) {
|
|
88
100
|
const option = inputOptions[key];
|
|
89
101
|
|
|
102
|
+
// Skip preset key—it’s already been processed
|
|
103
|
+
if (key === 'preset') {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
90
107
|
if (key === 'caseSensitive') {
|
|
91
108
|
if (option) {
|
|
92
109
|
options.name = identity;
|
|
@@ -201,7 +218,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
201
218
|
// Validate engine
|
|
202
219
|
const supportedEngines = ['terser', 'swc'];
|
|
203
220
|
if (!supportedEngines.includes(engine)) {
|
|
204
|
-
throw new Error(`Unsupported JS minifier engine:
|
|
221
|
+
throw new Error(`Unsupported JS minifier engine: “${engine}”. Supported engines: ${supportedEngines.join(', ')}`);
|
|
205
222
|
}
|
|
206
223
|
|
|
207
224
|
// Extract engine-specific options (excluding `engine` field itself)
|
|
@@ -308,14 +325,14 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
308
325
|
relateUrlOptions = {};
|
|
309
326
|
}
|
|
310
327
|
|
|
311
|
-
// Cache
|
|
328
|
+
// Cache relateurl instance for reuse (expensive to create)
|
|
312
329
|
const relateUrlInstance = new RelateURL(relateUrlOptions.site || '', relateUrlOptions);
|
|
313
330
|
|
|
314
331
|
// Create instance-specific cache (results depend on site configuration)
|
|
315
332
|
const instanceCache = urlMinifyCache ? new (urlMinifyCache.constructor)(500) : null;
|
|
316
333
|
|
|
317
334
|
options.minifyURLs = function (text) {
|
|
318
|
-
// Fast-path: Skip if text doesn
|
|
335
|
+
// Fast-path: Skip if text doesn’t look like a URL that needs processing
|
|
319
336
|
// Only process if contains URL-like characters (`/`, `:`, `#`, `?`) or spaces that need encoding
|
|
320
337
|
if (!/[/:?#\s]/.test(text)) {
|
|
321
338
|
return text;
|
|
@@ -347,17 +364,17 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
347
364
|
};
|
|
348
365
|
} else if (key === 'minifySVG') {
|
|
349
366
|
// Process SVG minification options
|
|
350
|
-
// Unlike minifyCSS
|
|
367
|
+
// Unlike `minifyCSS`/`minifyJS`, this is a simple options object, not a function
|
|
351
368
|
// The actual minification is applied inline during attribute processing
|
|
352
369
|
options.minifySVG = getSVGMinifierOptions(option);
|
|
353
370
|
} else if (key === 'customAttrCollapse') {
|
|
354
|
-
// Single
|
|
371
|
+
// Single regex pattern
|
|
355
372
|
options[key] = parseRegExp(option);
|
|
356
373
|
} else if (key === 'customAttrSurround') {
|
|
357
374
|
// Nested array of RegExp pairs: `[[openRegExp, closeRegExp], …]`
|
|
358
375
|
options[key] = parseNestedRegExpArray(option);
|
|
359
376
|
} else if (['customAttrAssign', 'customEventAttributes', 'ignoreCustomComments', 'ignoreCustomFragments'].includes(key)) {
|
|
360
|
-
// Array of
|
|
377
|
+
// Array of regex patterns
|
|
361
378
|
options[key] = parseRegExpArray(option);
|
|
362
379
|
} else {
|
|
363
380
|
options[key] = option;
|
package/src/lib/svg.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Lightweight SVG optimizations:
|
|
3
|
-
*
|
|
4
3
|
* - Numeric precision reduction for coordinates and path data
|
|
5
4
|
* - Whitespace removal in attribute values (numeric sequences)
|
|
6
5
|
* - Default attribute removal (safe, well-documented defaults)
|
|
@@ -9,6 +8,8 @@
|
|
|
9
8
|
* - Path data space optimization
|
|
10
9
|
*/
|
|
11
10
|
|
|
11
|
+
// Imports
|
|
12
|
+
|
|
12
13
|
import { LRU } from './utils.js';
|
|
13
14
|
import { RE_NUMERIC_VALUE } from './constants.js';
|
|
14
15
|
|
|
@@ -96,7 +97,7 @@ function minifyNumber(num, precision = 3) {
|
|
|
96
97
|
if (num === '1.0' || num === '1.00' || num === '1.000') return '1';
|
|
97
98
|
|
|
98
99
|
// Check cache
|
|
99
|
-
// (Note:
|
|
100
|
+
// (Note: Uses input string as key, so “0.0000” and “0.00000” create separate entries.
|
|
100
101
|
// This is intentional to avoid parsing overhead.
|
|
101
102
|
// Real-world SVG files from export tools typically use consistent formats.)
|
|
102
103
|
const cacheKey = `${num}:${precision}`;
|
|
@@ -135,15 +136,15 @@ function minifyPathData(pathData, precision = 3) {
|
|
|
135
136
|
|
|
136
137
|
// Remove unnecessary spaces around path commands
|
|
137
138
|
// Safe to remove space after a command letter when it’s followed by a number (which may be negative)
|
|
138
|
-
// M 10 20 → M10 20
|
|
139
|
+
// `M 10 20` → `M10 20`, `L -5 -3` → `L-5-3`
|
|
139
140
|
result = result.replace(/([MLHVCSQTAZmlhvcsqtaz])\s+(?=-?\d)/g, '$1');
|
|
140
141
|
|
|
141
142
|
// Safe to remove space before command letter when preceded by a number
|
|
142
|
-
// 0 L → 0L
|
|
143
|
+
// `0 L` → `0L`, `20 M` → `20M`
|
|
143
144
|
result = result.replace(/(\d)\s+([MLHVCSQTAZmlhvcsqtaz])/g, '$1$2');
|
|
144
145
|
|
|
145
146
|
// Safe to remove space before negative number when preceded by a number
|
|
146
|
-
// 10 -20 → 10-20 (numbers are separated by the minus sign)
|
|
147
|
+
// `10 -20` → `10-20` (numbers are separated by the minus sign)
|
|
147
148
|
result = result.replace(/(\d)\s+(-\d)/g, '$1$2');
|
|
148
149
|
|
|
149
150
|
return result;
|
|
@@ -152,9 +153,9 @@ function minifyPathData(pathData, precision = 3) {
|
|
|
152
153
|
/**
|
|
153
154
|
* Minify whitespace in numeric attribute values
|
|
154
155
|
* Examples:
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
156
|
+
* - “10 , 20" → "10,20"
|
|
157
|
+
* - "translate( 10 20 )" → "translate(10 20)"
|
|
158
|
+
* - "100, 10 40, 198" → "100,10 40,198"
|
|
158
159
|
*
|
|
159
160
|
* @param {string} value - Attribute value to minify
|
|
160
161
|
* @returns {string} Minified value
|
|
@@ -187,8 +188,7 @@ function minifyColor(color) {
|
|
|
187
188
|
|
|
188
189
|
// Don’t process values that aren’t simple colors (preserve case-sensitive references)
|
|
189
190
|
// `url(#id)`, `var(--name)`, `inherit`, `currentColor`, etc.
|
|
190
|
-
if (trimmed.includes('url(') || trimmed.includes('var(') ||
|
|
191
|
-
trimmed === 'inherit' || trimmed === 'currentColor') {
|
|
191
|
+
if (trimmed.includes('url(') || trimmed.includes('var(') || trimmed === 'inherit' || trimmed === 'currentColor') {
|
|
192
192
|
return trimmed;
|
|
193
193
|
}
|
|
194
194
|
|
|
@@ -196,7 +196,7 @@ function minifyColor(color) {
|
|
|
196
196
|
const lower = trimmed.toLowerCase();
|
|
197
197
|
|
|
198
198
|
// Shorten 6-digit hex to 3-digit when possible
|
|
199
|
-
//
|
|
199
|
+
// `#aabbcc` → `#abc`, `#000000` → `#000`
|
|
200
200
|
const hexMatch = lower.match(/^#([0-9a-f]{6})$/);
|
|
201
201
|
if (hexMatch) {
|
|
202
202
|
const hex = hexMatch[1];
|
|
@@ -216,7 +216,7 @@ function minifyColor(color) {
|
|
|
216
216
|
return NAMED_COLORS[lower] || lower;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
// Convert rgb(
|
|
219
|
+
// Convert rgb() to hex
|
|
220
220
|
const rgbMatch = lower.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
|
|
221
221
|
if (rgbMatch) {
|
|
222
222
|
const r = parseInt(rgbMatch[1], 10);
|
|
@@ -247,14 +247,14 @@ function minifyColor(color) {
|
|
|
247
247
|
const NUMERIC_ATTRS = new Set([
|
|
248
248
|
'd', // Path data
|
|
249
249
|
'points', // Polygon/polyline points
|
|
250
|
-
'viewBox', // viewBox coordinates
|
|
250
|
+
'viewBox', // `viewBox` coordinates
|
|
251
251
|
'transform', // Transform functions
|
|
252
252
|
'x', 'y', 'x1', 'y1', 'x2', 'y2', // Coordinates
|
|
253
253
|
'cx', 'cy', 'r', 'rx', 'ry', // Circle/ellipse
|
|
254
254
|
'width', 'height', // Dimensions
|
|
255
255
|
'dx', 'dy', // Text offsets
|
|
256
256
|
'offset', // Gradient offset
|
|
257
|
-
'startOffset', // textPath
|
|
257
|
+
'startOffset', // `textPath`
|
|
258
258
|
'pathLength', // Path length
|
|
259
259
|
'stdDeviation', // Filter params
|
|
260
260
|
'baseFrequency', // Turbulence
|
package/src/lib/whitespace.js
CHANGED
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
RE_NBSP_TRAILING_GROUP,
|
|
10
10
|
RE_NBSP_TRAILING_STRIP,
|
|
11
11
|
inlineElementsToKeepWhitespace,
|
|
12
|
-
inlineElementsToKeepWhitespaceWithin
|
|
12
|
+
inlineElementsToKeepWhitespaceWithin,
|
|
13
|
+
formControlElements
|
|
13
14
|
} from './constants.js';
|
|
14
15
|
|
|
15
16
|
// Trim whitespace
|
|
@@ -71,7 +72,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
if (trimLeft) {
|
|
74
|
-
//
|
|
75
|
+
// No-break space is specifically handled inside the replacer function
|
|
75
76
|
str = str.replace(/^[ \n\r\t\f\xA0]+/, function (spaces) {
|
|
76
77
|
const conservative = !lineBreakBefore && options.conservativeCollapse;
|
|
77
78
|
if (conservative && spaces === '\t') {
|
|
@@ -82,7 +83,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
if (trimRight) {
|
|
85
|
-
//
|
|
86
|
+
// No-break space is specifically handled inside the replacer function
|
|
86
87
|
str = str.replace(/[ \n\r\t\f\xA0]+$/, function (spaces) {
|
|
87
88
|
const conservative = !lineBreakAfter && options.conservativeCollapse;
|
|
88
89
|
if (conservative && spaces === '\t') {
|
|
@@ -106,11 +107,42 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
106
107
|
|
|
107
108
|
// Collapse whitespace smartly based on surrounding tags
|
|
108
109
|
|
|
109
|
-
function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements, inlineTextSet) {
|
|
110
|
+
function collapseWhitespaceSmart(str, prevTag, nextTag, prevAttrs, nextAttrs, options, inlineElements, inlineTextSet) {
|
|
111
|
+
const prevTagName = prevTag && (prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag);
|
|
112
|
+
const nextTagName = nextTag && (nextTag.charAt(0) === '/' ? nextTag.slice(1) : nextTag);
|
|
113
|
+
|
|
114
|
+
// Helper: Check if an input element has `type="hidden"`
|
|
115
|
+
const isHiddenInput = (tagName, attrs) => {
|
|
116
|
+
if (tagName !== 'input' || !attrs || !attrs.length) return false;
|
|
117
|
+
const typeAttr = attrs.find(attr => attr.name === 'type');
|
|
118
|
+
return typeAttr && typeAttr.value === 'hidden';
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Check if prev/next are non-rendering (hidden) elements
|
|
122
|
+
const prevIsHidden = isHiddenInput(prevTagName, prevAttrs);
|
|
123
|
+
const nextIsHidden = isHiddenInput(nextTagName, nextAttrs);
|
|
124
|
+
|
|
110
125
|
let trimLeft = prevTag && !inlineElementsToKeepWhitespace.has(prevTag);
|
|
126
|
+
|
|
127
|
+
// Smart default behavior: Collapse space after non-rendering elements (`type="hidden"`)
|
|
128
|
+
// This happens even in basic `collapseWhitespace` mode (safe optimization)
|
|
129
|
+
if (!trimLeft && prevIsHidden && str && !/\S/.test(str)) {
|
|
130
|
+
trimLeft = true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Aggressive mode: Collapse between all form controls (pure whitespace only)
|
|
134
|
+
const isPureWhitespace = str && !/\S/.test(str);
|
|
135
|
+
if (!trimLeft && prevTagName && nextTagName &&
|
|
136
|
+
options.collapseInlineTagWhitespace &&
|
|
137
|
+
isPureWhitespace &&
|
|
138
|
+
formControlElements.has(prevTagName) && formControlElements.has(nextTagName)) {
|
|
139
|
+
trimLeft = true;
|
|
140
|
+
}
|
|
141
|
+
|
|
111
142
|
if (trimLeft && !options.collapseInlineTagWhitespace) {
|
|
112
143
|
trimLeft = prevTag.charAt(0) === '/' ? !inlineElements.has(prevTag.slice(1)) : !inlineTextSet.has(prevTag);
|
|
113
144
|
}
|
|
145
|
+
|
|
114
146
|
// When `collapseInlineTagWhitespace` is enabled, still preserve whitespace around inline text elements
|
|
115
147
|
if (trimLeft && options.collapseInlineTagWhitespace) {
|
|
116
148
|
const tagName = prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag;
|
|
@@ -118,10 +150,26 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements,
|
|
|
118
150
|
trimLeft = false;
|
|
119
151
|
}
|
|
120
152
|
}
|
|
153
|
+
|
|
121
154
|
let trimRight = nextTag && !inlineElementsToKeepWhitespace.has(nextTag);
|
|
155
|
+
|
|
156
|
+
// Smart default behavior: Collapse space before non-rendering elements (`type="hidden"`)
|
|
157
|
+
if (!trimRight && nextIsHidden && str && !/\S/.test(str)) {
|
|
158
|
+
trimRight = true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Aggressive mode: Same as `trimLeft`
|
|
162
|
+
if (!trimRight && prevTagName && nextTagName &&
|
|
163
|
+
options.collapseInlineTagWhitespace &&
|
|
164
|
+
isPureWhitespace &&
|
|
165
|
+
formControlElements.has(prevTagName) && formControlElements.has(nextTagName)) {
|
|
166
|
+
trimRight = true;
|
|
167
|
+
}
|
|
168
|
+
|
|
122
169
|
if (trimRight && !options.collapseInlineTagWhitespace) {
|
|
123
170
|
trimRight = nextTag.charAt(0) === '/' ? !inlineTextSet.has(nextTag.slice(1)) : !inlineElements.has(nextTag);
|
|
124
171
|
}
|
|
172
|
+
|
|
125
173
|
// When `collapseInlineTagWhitespace` is enabled, still preserve whitespace around inline text elements
|
|
126
174
|
if (trimRight && options.collapseInlineTagWhitespace) {
|
|
127
175
|
const tagName = nextTag.charAt(0) === '/' ? nextTag.slice(1) : nextTag;
|
|
@@ -129,6 +177,7 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements,
|
|
|
129
177
|
trimRight = false;
|
|
130
178
|
}
|
|
131
179
|
}
|
|
180
|
+
|
|
132
181
|
return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
|
|
133
182
|
}
|
|
134
183
|
|
package/src/presets.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Preset configurations
|
|
2
|
+
* Preset configurations
|
|
3
3
|
*
|
|
4
4
|
* Presets provide curated option sets for common use cases:
|
|
5
|
-
* - conservative
|
|
6
|
-
* - comprehensive
|
|
5
|
+
* - `conservative`: Safe minification suitable for most projects
|
|
6
|
+
* - `comprehensive`: Aggressive minification for maximum file size reduction
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export const presets = {
|
|
@@ -24,7 +24,6 @@ export const presets = {
|
|
|
24
24
|
comprehensive: {
|
|
25
25
|
caseSensitive: true,
|
|
26
26
|
collapseBooleanAttributes: true,
|
|
27
|
-
collapseInlineTagWhitespace: true,
|
|
28
27
|
collapseWhitespace: true,
|
|
29
28
|
continueOnParseError: true,
|
|
30
29
|
decodeEntities: true,
|
|
@@ -49,7 +48,7 @@ export const presets = {
|
|
|
49
48
|
|
|
50
49
|
/**
|
|
51
50
|
* Get preset configuration by name
|
|
52
|
-
* @param {string} name - Preset name (
|
|
51
|
+
* @param {string} name - Preset name (“conservative” or “comprehensive”)
|
|
53
52
|
* @returns {object|null} Preset options object or null if not found
|
|
54
53
|
*/
|
|
55
54
|
export function getPreset(name) {
|
package/src/tokenchain.js
CHANGED
|
@@ -35,7 +35,7 @@ class Sorter {
|
|
|
35
35
|
|
|
36
36
|
class TokenChain {
|
|
37
37
|
constructor() {
|
|
38
|
-
// Use
|
|
38
|
+
// Use map instead of object properties for better performance
|
|
39
39
|
this.map = new Map();
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -52,7 +52,7 @@ class TokenChain {
|
|
|
52
52
|
const sorter = new Sorter();
|
|
53
53
|
sorter.sorterMap = new Map();
|
|
54
54
|
|
|
55
|
-
// Convert
|
|
55
|
+
// Convert map entries to array and sort by frequency (descending), then alphabetically
|
|
56
56
|
const entries = Array.from(this.map.entries()).sort((a, b) => {
|
|
57
57
|
const m = a[1].arrays.length;
|
|
58
58
|
const n = b[1].arrays.length;
|
package/src/lib/index.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
// Utils
|
|
2
|
-
export * from './utils.js';
|
|
3
|
-
|
|
4
|
-
// Constants
|
|
5
|
-
export * from './constants.js';
|
|
6
|
-
|
|
7
|
-
// Whitespace
|
|
8
|
-
export * from './whitespace.js';
|
|
9
|
-
|
|
10
|
-
// Attributes
|
|
11
|
-
export * from './attributes.js';
|
|
12
|
-
|
|
13
|
-
// Elements
|
|
14
|
-
export * from './elements.js';
|
|
15
|
-
|
|
16
|
-
// Content processors
|
|
17
|
-
export * from './content.js';
|
|
18
|
-
|
|
19
|
-
// Options
|
|
20
|
-
export * from './options.js';
|