html-minifier 3.5.21 → 5.0.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/LICENSE.txt +23 -0
- package/README.md +851 -109
- package/cli.js +991 -206
- package/dist/types/htmlminifier.d.ts +629 -0
- package/dist/types/htmlminifier.d.ts.map +1 -0
- package/dist/types/htmlparser.d.ts +28 -0
- package/dist/types/htmlparser.d.ts.map +1 -0
- package/dist/types/lib/attributes.d.ts +87 -0
- package/dist/types/lib/attributes.d.ts.map +1 -0
- package/dist/types/lib/constants.d.ts +421 -0
- package/dist/types/lib/constants.d.ts.map +1 -0
- package/dist/types/lib/content.d.ts +44 -0
- package/dist/types/lib/content.d.ts.map +1 -0
- package/dist/types/lib/elements.d.ts +48 -0
- package/dist/types/lib/elements.d.ts.map +1 -0
- package/dist/types/lib/fragments.d.ts +47 -0
- package/dist/types/lib/fragments.d.ts.map +1 -0
- package/dist/types/lib/option-definitions.d.ts +33 -0
- package/dist/types/lib/option-definitions.d.ts.map +1 -0
- package/dist/types/lib/options.d.ts +93 -0
- package/dist/types/lib/options.d.ts.map +1 -0
- package/dist/types/lib/unused-css.d.ts +76 -0
- package/dist/types/lib/unused-css.d.ts.map +1 -0
- package/dist/types/lib/urls.d.ts +15 -0
- package/dist/types/lib/urls.d.ts.map +1 -0
- package/dist/types/lib/utils.d.ts +132 -0
- package/dist/types/lib/utils.d.ts.map +1 -0
- package/dist/types/lib/whitespace.d.ts +97 -0
- package/dist/types/lib/whitespace.d.ts.map +1 -0
- package/dist/types/presets.d.ts +50 -0
- package/dist/types/presets.d.ts.map +1 -0
- package/dist/types/tokenchain.d.ts +36 -0
- package/dist/types/tokenchain.d.ts.map +1 -0
- package/html-minifier.schema.json +322 -0
- package/package.json +70 -58
- package/src/htmlminifier.js +2848 -1095
- package/src/htmlparser.js +1165 -450
- package/src/lib/attributes.js +1196 -0
- package/src/lib/constants.js +521 -0
- package/src/lib/content.js +237 -0
- package/src/lib/elements.js +268 -0
- package/src/lib/file-pool.js +193 -0
- package/src/lib/file-write.js +87 -0
- package/src/lib/fragments.js +340 -0
- package/src/lib/option-definitions.js +246 -0
- package/src/lib/options.js +841 -0
- package/src/lib/unused-css.js +499 -0
- package/src/lib/urls.js +146 -0
- package/src/lib/utils.js +1026 -0
- package/src/lib/whitespace.js +431 -0
- package/src/presets.js +58 -0
- package/src/tokenchain.js +142 -56
- package/LICENSE +0 -22
- package/sample-cli-config-file.conf +0 -39
- package/src/utils.js +0 -18
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whitespace trimming and collapsing
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
RE_ALL_WS_NBSP,
|
|
7
|
+
RE_NBSP_LEADING_GROUP,
|
|
8
|
+
RE_NBSP_LEAD_GROUP,
|
|
9
|
+
RE_NBSP_TRAILING_GROUP,
|
|
10
|
+
RE_NBSP_TRAILING_STRIP,
|
|
11
|
+
inlineElementsToKeepWhitespace,
|
|
12
|
+
inlineElementsToKeepWhitespaceAround,
|
|
13
|
+
inlineElementsToKeepWhitespaceWithin,
|
|
14
|
+
inlineElementsToKeepWhitespaceBetween,
|
|
15
|
+
inlineElementsToKeepWhitespaceBetweenEnd,
|
|
16
|
+
inlineElementsTransparent,
|
|
17
|
+
inlineElementsTransparentAfter,
|
|
18
|
+
formControlElementsEither,
|
|
19
|
+
toEndTags
|
|
20
|
+
} from './constants.js';
|
|
21
|
+
|
|
22
|
+
const RE_ANY_WS_NBSP = /[ \n\r\t\f\xA0]/;
|
|
23
|
+
const RE_TAB_NBSP = /[\t\xA0]/;
|
|
24
|
+
const RE_ASCII_WS_RUN = /[ \n\r\f]+/g;
|
|
25
|
+
// Every reason `collapseWhitespaceAllKnown` has to do any work at all, in one scan
|
|
26
|
+
const RE_COLLAPSIBLE = /[\t\xA0\n\r\f]| {2}/;
|
|
27
|
+
const RE_NON_WS = /\S/;
|
|
28
|
+
// Any no-break space character (the `collapseNoBreakSpaces` early-exit scan)
|
|
29
|
+
const RE_NO_BREAK_SPACE_CHARS = /[\xA0\u202F\u2007]/;
|
|
30
|
+
|
|
31
|
+
/** @param {string} str */
|
|
32
|
+
const trimWhitespace = str => {
|
|
33
|
+
if (!str) return str;
|
|
34
|
+
let start = 0;
|
|
35
|
+
let end = str.length;
|
|
36
|
+
while (start < end && isAsciiWs(str.charCodeAt(start))) start++;
|
|
37
|
+
while (end > start && isAsciiWs(str.charCodeAt(end - 1))) end--;
|
|
38
|
+
return start === 0 && end === str.length ? str : str.slice(start, end);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** @param {number} code */
|
|
42
|
+
function isAsciiWs(code) {
|
|
43
|
+
return code === 32 || code === 10 || code === 13 || code === 9 || code === 12;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @param {number} code */
|
|
47
|
+
function isWsOrNbsp(code) {
|
|
48
|
+
return code === 32 || code === 10 || code === 13 || code === 9 || code === 12 || code === 160;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Whether the string is empty or holds only what `\s` matches, scanned by code,
|
|
52
|
+
// as text mostly ends the scan on its first character (where a regex costs more)
|
|
53
|
+
/** @param {string} str */
|
|
54
|
+
function isAllWhitespace(str) {
|
|
55
|
+
for (let i = 0; i < str.length; i++) {
|
|
56
|
+
const code = str.charCodeAt(i);
|
|
57
|
+
if (!((code >= 9 && code <= 13) || code === 32 || code === 0xA0 || code === 0x1680 ||
|
|
58
|
+
(code >= 0x2000 && code <= 0x200A) || code === 0x2028 || code === 0x2029 ||
|
|
59
|
+
code === 0x202F || code === 0x205F || code === 0x3000 || code === 0xFEFF)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Whether the last character is whitespace, no-break space included
|
|
67
|
+
/** @param {string} str */
|
|
68
|
+
function endsWithWhitespace(str) {
|
|
69
|
+
return str.length > 0 && isWsOrNbsp(str.charCodeAt(str.length - 1));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Module scope so the hot path doesn’t allocate a closure per call
|
|
73
|
+
/** @param {string} spaces */
|
|
74
|
+
function collapseRun(spaces) {
|
|
75
|
+
if (spaces === '\t') return '\t';
|
|
76
|
+
if (spaces.indexOf('\xA0') === -1) return ' ';
|
|
77
|
+
return spaces.replace(RE_NBSP_LEADING_GROUP, '$1 ');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** @param {string} str */
|
|
81
|
+
function collapseWhitespaceAll(str) {
|
|
82
|
+
if (!str) return str;
|
|
83
|
+
// Fast path: If there are no common whitespace characters, return early
|
|
84
|
+
if (!RE_ANY_WS_NBSP.test(str)) {
|
|
85
|
+
return str;
|
|
86
|
+
}
|
|
87
|
+
return collapseWhitespaceAllKnown(str);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// As `collapseWhitespaceAll`, for callers that already know `str` holds whitespace
|
|
91
|
+
/** @param {string} str */
|
|
92
|
+
function collapseWhitespaceAllKnown(str) {
|
|
93
|
+
// Most text reaching here is already collapsed, so settle that in one scan rather
|
|
94
|
+
// than two—single spaces with no tab and no no-break space leave nothing to do
|
|
95
|
+
if (!RE_COLLAPSIBLE.test(str)) {
|
|
96
|
+
return str;
|
|
97
|
+
}
|
|
98
|
+
// Only a tab or a no-break space makes the replacement depend on the run itself;
|
|
99
|
+
// without either, every run becomes one space and no per-match callback is needed
|
|
100
|
+
if (!RE_TAB_NBSP.test(str)) {
|
|
101
|
+
return str.replace(RE_ASCII_WS_RUN, ' ');
|
|
102
|
+
}
|
|
103
|
+
return str.replace(RE_ALL_WS_NBSP, collapseRun);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Collapse whitespace into adjacent no-break spaces
|
|
107
|
+
|
|
108
|
+
// A no-break space (`U+00A0`), narrow no-break space (`U+202F`), or figure space (`U+2007`),
|
|
109
|
+
// literal or as a character reference; numeric references and ` ` also work without
|
|
110
|
+
// the semicolon in text
|
|
111
|
+
const RE_NO_BREAK_SPACE_WS = /([ \n\r\t\f]*)(\xA0|\u202F|\u2007|&(?:nbsp;?|NonBreakingSpace;|numsp;|#0*(?:160|8239|8199)(?![0-9]);?|#[xX]0*(?:[aA]0|202[fF]|2007)(?![0-9a-fA-F]);?))([ \n\r\t\f]*)/g;
|
|
112
|
+
// Decoded text holds references only as text, so there the characters alone count
|
|
113
|
+
const RE_NO_BREAK_SPACE_CHAR_WS = /([ \n\r\t\f]*)([\xA0\u202F\u2007])([ \n\r\t\f]*)/g;
|
|
114
|
+
const RE_LINE_BREAK = /[\n\r]/;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {string} _match
|
|
118
|
+
* @param {string} before
|
|
119
|
+
* @param {string} noBreakSpace
|
|
120
|
+
* @param {string} after
|
|
121
|
+
*/
|
|
122
|
+
function keepLineBreaks(_match, before, noBreakSpace, after) {
|
|
123
|
+
return (RE_LINE_BREAK.test(before) ? before : '') + noBreakSpace + (RE_LINE_BREAK.test(after) ? after : '');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Callers pass text whose whitespace runs are already collapsed, as each match
|
|
127
|
+
// rescans the run in front of it
|
|
128
|
+
/**
|
|
129
|
+
* @param {string} str
|
|
130
|
+
* @param {{decodeEntities?: boolean | undefined, preserveLineBreaks?: boolean | undefined}} options
|
|
131
|
+
*/
|
|
132
|
+
function collapseNoBreakSpaces(str, options) {
|
|
133
|
+
const decoded = Boolean(options.decodeEntities);
|
|
134
|
+
// One scan for all three no-break space characters, rather than an `indexOf` per character
|
|
135
|
+
if (!str || (!RE_NO_BREAK_SPACE_CHARS.test(str) && (decoded || str.indexOf('&') === -1))) {
|
|
136
|
+
return str;
|
|
137
|
+
}
|
|
138
|
+
const pattern = decoded ? RE_NO_BREAK_SPACE_CHAR_WS : RE_NO_BREAK_SPACE_WS;
|
|
139
|
+
if (options.preserveLineBreaks) {
|
|
140
|
+
return str.replace(pattern, keepLineBreaks);
|
|
141
|
+
}
|
|
142
|
+
return str.replace(pattern, '$2');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Collapse whitespace with options
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {string} str
|
|
149
|
+
* @param {{preserveLineBreaks?: boolean | undefined, conservativeCollapse?: boolean | undefined}} options
|
|
150
|
+
* @param {boolean} trimLeft
|
|
151
|
+
* @param {boolean} trimRight
|
|
152
|
+
* @param {boolean} [collapseAll]
|
|
153
|
+
* @param {boolean} [keepUntrimmed] - Whether a side not trimmed keeps its line breaks as written
|
|
154
|
+
*/
|
|
155
|
+
function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll = false, keepUntrimmed = false) {
|
|
156
|
+
if (!str) return str;
|
|
157
|
+
|
|
158
|
+
// Fast path: Nothing to do
|
|
159
|
+
if (!trimLeft && !trimRight && !collapseAll && (!options.preserveLineBreaks || keepUntrimmed)) {
|
|
160
|
+
return str;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Fast path: No whitespace at all
|
|
164
|
+
if (!RE_ANY_WS_NBSP.test(str)) {
|
|
165
|
+
return str;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return collapseWhitespaceKnown(str, options, trimLeft, trimRight, collapseAll, keepUntrimmed);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// As `collapseWhitespace`, for callers that already know `str` is non-empty, holds
|
|
172
|
+
// whitespace, and has something to do
|
|
173
|
+
/**
|
|
174
|
+
* @param {string} str
|
|
175
|
+
* @param {{preserveLineBreaks?: boolean | undefined, conservativeCollapse?: boolean | undefined}} options
|
|
176
|
+
* @param {boolean} trimLeft
|
|
177
|
+
* @param {boolean} trimRight
|
|
178
|
+
* @param {boolean} collapseAll
|
|
179
|
+
* @param {boolean} [keepUntrimmed] - Whether a side not trimmed keeps its line breaks as written
|
|
180
|
+
*/
|
|
181
|
+
function collapseWhitespaceKnown(str, options, trimLeft, trimRight, collapseAll, keepUntrimmed = false) {
|
|
182
|
+
let lineBreakBefore = ''; let lineBreakAfter = '';
|
|
183
|
+
|
|
184
|
+
if (options.preserveLineBreaks) {
|
|
185
|
+
// Find leading/trailing whitespace containing line breaks manually
|
|
186
|
+
// (avoids polynomial backtracking with end-anchored lazy quantifiers);
|
|
187
|
+
// the line-break check scans the run’s char codes directly rather than
|
|
188
|
+
// testing a sliced copy, so the common case allocates nothing
|
|
189
|
+
if (trimLeft || !keepUntrimmed) {
|
|
190
|
+
let leadEnd = 0;
|
|
191
|
+
let hasLineBreak = false;
|
|
192
|
+
for (; leadEnd < str.length; leadEnd++) {
|
|
193
|
+
const code = str.charCodeAt(leadEnd);
|
|
194
|
+
if (!isAsciiWs(code)) break;
|
|
195
|
+
// Settling the line break in the same pass keeps the run from being walked twice
|
|
196
|
+
if (code === 10 || code === 13) hasLineBreak = true;
|
|
197
|
+
}
|
|
198
|
+
if (hasLineBreak) {
|
|
199
|
+
lineBreakBefore = '\n';
|
|
200
|
+
str = str.slice(leadEnd);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (trimRight || !keepUntrimmed) {
|
|
204
|
+
let trailStart = str.length;
|
|
205
|
+
let hasLineBreak = false;
|
|
206
|
+
for (; trailStart > 0; trailStart--) {
|
|
207
|
+
const code = str.charCodeAt(trailStart - 1);
|
|
208
|
+
if (!isAsciiWs(code)) break;
|
|
209
|
+
if (code === 10 || code === 13) hasLineBreak = true;
|
|
210
|
+
}
|
|
211
|
+
if (hasLineBreak) {
|
|
212
|
+
lineBreakAfter = '\n';
|
|
213
|
+
str = str.slice(0, trailStart);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (trimLeft) {
|
|
219
|
+
// Find the leading whitespace boundary with a loop, so the common case neither
|
|
220
|
+
// allocates a replacer closure nor runs the regex machinery
|
|
221
|
+
let start = 0;
|
|
222
|
+
while (start < str.length && isWsOrNbsp(str.charCodeAt(start))) {
|
|
223
|
+
start++;
|
|
224
|
+
}
|
|
225
|
+
if (start > 0) {
|
|
226
|
+
const spaces = str.slice(0, start);
|
|
227
|
+
const conservative = !lineBreakBefore && options.conservativeCollapse;
|
|
228
|
+
let replacement;
|
|
229
|
+
if (conservative && spaces === '\t') {
|
|
230
|
+
replacement = '\t';
|
|
231
|
+
} else if (spaces.indexOf('\xA0') === -1) {
|
|
232
|
+
// No no-break space: The whole run goes
|
|
233
|
+
replacement = conservative ? ' ' : '';
|
|
234
|
+
} else {
|
|
235
|
+
// No-break space is specifically handled via the nested regexes
|
|
236
|
+
replacement = spaces.replace(/^[^\xA0]+/, '').replace(RE_NBSP_LEAD_GROUP, '$1 ') || (conservative ? ' ' : '');
|
|
237
|
+
}
|
|
238
|
+
str = replacement + str.slice(start);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (trimRight) {
|
|
243
|
+
// Find trailing whitespace boundary manually (avoids polynomial backtracking
|
|
244
|
+
// with `/[ \n\r\t\f\xA0]+$/` on strings with long internal whitespace runs)
|
|
245
|
+
let end = str.length;
|
|
246
|
+
while (end > 0 && isWsOrNbsp(str.charCodeAt(end - 1))) {
|
|
247
|
+
end--;
|
|
248
|
+
}
|
|
249
|
+
if (end < str.length) {
|
|
250
|
+
const spaces = str.slice(end);
|
|
251
|
+
const conservative = !lineBreakAfter && options.conservativeCollapse;
|
|
252
|
+
let replacement;
|
|
253
|
+
if (conservative && spaces === '\t') {
|
|
254
|
+
replacement = '\t';
|
|
255
|
+
} else if (spaces.indexOf('\xA0') === -1) {
|
|
256
|
+
// No no-break space: The whole run goes
|
|
257
|
+
replacement = conservative ? ' ' : '';
|
|
258
|
+
} else {
|
|
259
|
+
// No-break space is specifically handled via the nested regexes
|
|
260
|
+
replacement = spaces.replace(RE_NBSP_TRAILING_GROUP, ' $1').replace(RE_NBSP_TRAILING_STRIP, '') || (conservative ? ' ' : '');
|
|
261
|
+
}
|
|
262
|
+
str = str.slice(0, end) + replacement;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (collapseAll && str) {
|
|
267
|
+
// Strip non-space whitespace then compress spaces to one
|
|
268
|
+
str = collapseWhitespaceAllKnown(str);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Avoid string concatenation when no line breaks (common case)
|
|
272
|
+
if (!lineBreakBefore && !lineBreakAfter) return str;
|
|
273
|
+
if (!lineBreakBefore) return str + lineBreakAfter;
|
|
274
|
+
if (!lineBreakAfter) return lineBreakBefore + str;
|
|
275
|
+
return lineBreakBefore + str + lineBreakAfter;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Collapse whitespace smartly based on surrounding tags
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* @typedef {object} InlineSets
|
|
282
|
+
* @prop {Set<string>} around - Elements whose surrounding whitespace is kept
|
|
283
|
+
* @prop {Set<string>} aroundEnd - The same, spelled as end tags
|
|
284
|
+
* @prop {Set<string>} within - Elements whose inner whitespace is kept
|
|
285
|
+
* @prop {Set<string>} withinEnd - The same, spelled as end tags
|
|
286
|
+
* @prop {Set<string>} withinEither - `within` under both spellings
|
|
287
|
+
*/
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Inline-element membership for the whitespace pass, each set paired with its end-tag
|
|
291
|
+
* spelling so a lookup takes the tag as it comes rather than slicing off the slash
|
|
292
|
+
* @param {Set<string>} around
|
|
293
|
+
* @param {Set<string>} within
|
|
294
|
+
* @returns {InlineSets}
|
|
295
|
+
*/
|
|
296
|
+
function buildInlineSets(around, within) {
|
|
297
|
+
const withinEnd = toEndTags(within);
|
|
298
|
+
return {
|
|
299
|
+
around,
|
|
300
|
+
aroundEnd: toEndTags(around),
|
|
301
|
+
within,
|
|
302
|
+
withinEnd,
|
|
303
|
+
withinEither: new Set([...within, ...withinEnd])
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// The sets for a document without custom inline elements, built once
|
|
308
|
+
const defaultInlineSets = buildInlineSets(inlineElementsToKeepWhitespaceAround, inlineElementsToKeepWhitespaceWithin);
|
|
309
|
+
|
|
310
|
+
// Check if an input element has `type="hidden"`; module-scope so the hot path
|
|
311
|
+
// doesn’t allocate a closure per text node
|
|
312
|
+
/**
|
|
313
|
+
* @param {Array<{name: string, value?: string}>} attrs
|
|
314
|
+
*/
|
|
315
|
+
function isHiddenInput(attrs) {
|
|
316
|
+
if (!attrs || !attrs.length) return false;
|
|
317
|
+
for (const attr of attrs) {
|
|
318
|
+
// Attribute names and the `type` keyword are both ASCII case-insensitive
|
|
319
|
+
if (attr.name.toLowerCase() === 'type') {
|
|
320
|
+
return attr.value?.toLowerCase() === 'hidden';
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* @param {string} str
|
|
328
|
+
* @param {string} prevTag
|
|
329
|
+
* @param {string} nextTag
|
|
330
|
+
* @param {Array<{name: string, value?: string}>} prevAttrs
|
|
331
|
+
* @param {Array<{name: string, value?: string}>} nextAttrs
|
|
332
|
+
* @param {{preserveLineBreaks?: boolean, conservativeCollapse?: boolean, collapseInlineTagWhitespace?: boolean}} options
|
|
333
|
+
* @param {InlineSets} inlineSets
|
|
334
|
+
* @param {boolean} [collapseInside] - False where `canCollapseWhitespace` keeps runs inside the text as they are
|
|
335
|
+
*/
|
|
336
|
+
function collapseWhitespaceSmart(str, prevTag, nextTag, prevAttrs, nextAttrs, options, inlineSets, collapseInside = true) {
|
|
337
|
+
if (!str) return str;
|
|
338
|
+
|
|
339
|
+
// Fast path: No whitespace at all—every decision below would leave `str`
|
|
340
|
+
// untouched (`collapseWhitespace` returns such strings unchanged)
|
|
341
|
+
if (!RE_ANY_WS_NBSP.test(str)) {
|
|
342
|
+
return str;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let trimLeft = Boolean(prevTag) && !inlineElementsToKeepWhitespace.has(prevTag);
|
|
346
|
+
let trimRight = Boolean(nextTag) && !inlineElementsToKeepWhitespace.has(nextTag);
|
|
347
|
+
|
|
348
|
+
const prevTransparent = inlineElementsTransparentAfter.has(prevTag);
|
|
349
|
+
const nextTransparent = inlineElementsTransparent.has(nextTag);
|
|
350
|
+
|
|
351
|
+
// Every branch below that consults the text’s content needs a side left untrimmed, an element
|
|
352
|
+
// that doesn’t render, or `collapseInlineTagWhitespace`, so the scan for a non-whitespace
|
|
353
|
+
// character is only worth making then
|
|
354
|
+
const isPureWhitespace = (!trimLeft || !trimRight || prevTransparent || nextTransparent || Boolean(options.collapseInlineTagWhitespace)) && !RE_NON_WS.test(str);
|
|
355
|
+
|
|
356
|
+
// Next to text, spacing is part of the text, so aggressive collapsing only applies between tags
|
|
357
|
+
const inlineOption = Boolean(options.collapseInlineTagWhitespace) && isPureWhitespace;
|
|
358
|
+
|
|
359
|
+
if (isPureWhitespace) {
|
|
360
|
+
// Smart default behavior: Collapse space around non-rendering elements
|
|
361
|
+
// (`type="hidden"`); this happens even in basic `collapseWhitespace` mode
|
|
362
|
+
if (!trimLeft && prevTag === 'input' && isHiddenInput(prevAttrs)) {
|
|
363
|
+
trimLeft = true;
|
|
364
|
+
}
|
|
365
|
+
if (!trimRight && nextTag === 'input' && isHiddenInput(nextAttrs)) {
|
|
366
|
+
trimRight = true;
|
|
367
|
+
}
|
|
368
|
+
// Aggressive mode: Collapse between all form controls
|
|
369
|
+
if (inlineOption && (!trimLeft || !trimRight) &&
|
|
370
|
+
formControlElementsEither.has(prevTag) && formControlElementsEither.has(nextTag)) {
|
|
371
|
+
trimLeft = true;
|
|
372
|
+
trimRight = true;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Aggressive mode keeps whitespace around standard text-level elements only, not
|
|
377
|
+
// around all inline elements
|
|
378
|
+
if (trimLeft) {
|
|
379
|
+
trimLeft = prevTag.charCodeAt(0) === 47 /* / */ ? !(inlineOption ? inlineElementsToKeepWhitespaceBetweenEnd : inlineSets.aroundEnd).has(prevTag) : !inlineSets.within.has(prevTag);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (trimRight) {
|
|
383
|
+
trimRight = nextTag.charCodeAt(0) === 47 /* / */ ? !inlineSets.withinEnd.has(nextTag) : !(inlineOption ? inlineElementsToKeepWhitespaceBetween : inlineSets.around).has(nextTag);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Next to an element that doesn’t render, whitespace may separate what lies beyond it, so the
|
|
387
|
+
// element leaves the decision to the text or to a tag on the other side that renders
|
|
388
|
+
if (prevTransparent && (!isPureWhitespace || (Boolean(nextTag) && !nextTransparent))) {
|
|
389
|
+
trimLeft = false;
|
|
390
|
+
}
|
|
391
|
+
if (nextTransparent && (!isPureWhitespace || (Boolean(prevTag) && !prevTransparent))) {
|
|
392
|
+
trimRight = false;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const collapseAll = collapseInside && Boolean(prevTag && nextTag);
|
|
396
|
+
if (!trimLeft && !trimRight && !collapseAll && (!options.preserveLineBreaks || !collapseInside)) {
|
|
397
|
+
return str;
|
|
398
|
+
}
|
|
399
|
+
return collapseWhitespaceKnown(str, options, trimLeft, trimRight, collapseAll, !collapseInside);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Collapse/trim whitespace for given tag
|
|
403
|
+
|
|
404
|
+
// `xmp`, `listing`, and `plaintext` render preformatted, as `pre` does
|
|
405
|
+
// https://html.spec.whatwg.org/multipage/rendering.html#the-page
|
|
406
|
+
const noCollapseWhitespaceTags = new Set(['script', 'style', 'pre', 'textarea', 'xmp', 'listing', 'plaintext']);
|
|
407
|
+
const noTrimWhitespaceTags = new Set(['pre', 'textarea', 'xmp', 'listing', 'plaintext']);
|
|
408
|
+
|
|
409
|
+
/** @param {string} tag */
|
|
410
|
+
function canCollapseWhitespace(tag) {
|
|
411
|
+
return !noCollapseWhitespaceTags.has(tag);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** @param {string} tag */
|
|
415
|
+
function canTrimWhitespace(tag) {
|
|
416
|
+
return !noTrimWhitespaceTags.has(tag);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export {
|
|
420
|
+
buildInlineSets,
|
|
421
|
+
defaultInlineSets,
|
|
422
|
+
endsWithWhitespace,
|
|
423
|
+
trimWhitespace,
|
|
424
|
+
collapseWhitespaceAll,
|
|
425
|
+
collapseNoBreakSpaces,
|
|
426
|
+
collapseWhitespace,
|
|
427
|
+
collapseWhitespaceSmart,
|
|
428
|
+
canCollapseWhitespace,
|
|
429
|
+
canTrimWhitespace,
|
|
430
|
+
isAllWhitespace
|
|
431
|
+
};
|
package/src/presets.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preset configurations
|
|
3
|
+
*
|
|
4
|
+
* Presets provide curated option sets for common use cases:
|
|
5
|
+
* - `conservative`: Safe minification suitable for most projects
|
|
6
|
+
* - `comprehensive`: Aggressive minification for maximum file size reduction
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const presets = {
|
|
10
|
+
conservative: {
|
|
11
|
+
caseSensitive: true,
|
|
12
|
+
collapseBooleanAttributes: true,
|
|
13
|
+
collapseWhitespace: true,
|
|
14
|
+
conservativeCollapse: true,
|
|
15
|
+
preserveLineBreaks: true,
|
|
16
|
+
removeComments: true,
|
|
17
|
+
removeDefaultTypeAttributes: true,
|
|
18
|
+
useShortDoctype: true
|
|
19
|
+
},
|
|
20
|
+
comprehensive: {
|
|
21
|
+
collapseAttributeWhitespace: true,
|
|
22
|
+
collapseBooleanAttributes: true,
|
|
23
|
+
collapseEmptyAttributes: true,
|
|
24
|
+
collapseWhitespace: true,
|
|
25
|
+
decodeEntities: true,
|
|
26
|
+
mergeScripts: true,
|
|
27
|
+
minifyCSS: true,
|
|
28
|
+
minifyJS: true,
|
|
29
|
+
minifyJSON: true,
|
|
30
|
+
minifySVG: true,
|
|
31
|
+
minifyURLs: true,
|
|
32
|
+
removeAttributeQuotes: true,
|
|
33
|
+
removeComments: true,
|
|
34
|
+
removeDefaultTypeAttributes: true,
|
|
35
|
+
removeEmptyAttributes: true,
|
|
36
|
+
removeOptionalTags: true,
|
|
37
|
+
removeRedundantAttributes: true,
|
|
38
|
+
selfCloseForeignElements: true,
|
|
39
|
+
useShortDoctype: true
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} name - Preset name (“conservative” or “comprehensive”)
|
|
45
|
+
* @returns {object|null} Preset options object or null if not found
|
|
46
|
+
*/
|
|
47
|
+
export function getPreset(name) {
|
|
48
|
+
if (!name) return null;
|
|
49
|
+
const normalizedName = name.toLowerCase();
|
|
50
|
+
return /** @type {Record<string, object>} */ (presets)[normalizedName] || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @returns {string[]} Array of preset names
|
|
55
|
+
*/
|
|
56
|
+
export function getPresetNames() {
|
|
57
|
+
return Object.keys(presets);
|
|
58
|
+
}
|