html-minifier 4.0.0 → 5.0.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.
Files changed (55) hide show
  1. package/LICENSE.txt +23 -0
  2. package/README.md +851 -110
  3. package/cli.js +991 -207
  4. package/dist/types/htmlminifier.d.ts +629 -0
  5. package/dist/types/htmlminifier.d.ts.map +1 -0
  6. package/dist/types/htmlparser.d.ts +28 -0
  7. package/dist/types/htmlparser.d.ts.map +1 -0
  8. package/dist/types/lib/attributes.d.ts +87 -0
  9. package/dist/types/lib/attributes.d.ts.map +1 -0
  10. package/dist/types/lib/constants.d.ts +421 -0
  11. package/dist/types/lib/constants.d.ts.map +1 -0
  12. package/dist/types/lib/content.d.ts +44 -0
  13. package/dist/types/lib/content.d.ts.map +1 -0
  14. package/dist/types/lib/elements.d.ts +48 -0
  15. package/dist/types/lib/elements.d.ts.map +1 -0
  16. package/dist/types/lib/fragments.d.ts +47 -0
  17. package/dist/types/lib/fragments.d.ts.map +1 -0
  18. package/dist/types/lib/option-definitions.d.ts +33 -0
  19. package/dist/types/lib/option-definitions.d.ts.map +1 -0
  20. package/dist/types/lib/options.d.ts +93 -0
  21. package/dist/types/lib/options.d.ts.map +1 -0
  22. package/dist/types/lib/unused-css.d.ts +76 -0
  23. package/dist/types/lib/unused-css.d.ts.map +1 -0
  24. package/dist/types/lib/urls.d.ts +15 -0
  25. package/dist/types/lib/urls.d.ts.map +1 -0
  26. package/dist/types/lib/utils.d.ts +132 -0
  27. package/dist/types/lib/utils.d.ts.map +1 -0
  28. package/dist/types/lib/whitespace.d.ts +97 -0
  29. package/dist/types/lib/whitespace.d.ts.map +1 -0
  30. package/dist/types/presets.d.ts +50 -0
  31. package/dist/types/presets.d.ts.map +1 -0
  32. package/dist/types/tokenchain.d.ts +36 -0
  33. package/dist/types/tokenchain.d.ts.map +1 -0
  34. package/html-minifier.schema.json +322 -0
  35. package/package.json +71 -60
  36. package/src/htmlminifier.js +2862 -1106
  37. package/src/htmlparser.js +1163 -452
  38. package/src/lib/attributes.js +1196 -0
  39. package/src/lib/constants.js +521 -0
  40. package/src/lib/content.js +237 -0
  41. package/src/lib/elements.js +268 -0
  42. package/src/lib/file-pool.js +193 -0
  43. package/src/lib/file-write.js +87 -0
  44. package/src/lib/fragments.js +340 -0
  45. package/src/lib/option-definitions.js +246 -0
  46. package/src/lib/options.js +841 -0
  47. package/src/lib/unused-css.js +499 -0
  48. package/src/lib/urls.js +146 -0
  49. package/src/lib/utils.js +1026 -0
  50. package/src/lib/whitespace.js +431 -0
  51. package/src/presets.js +58 -0
  52. package/src/tokenchain.js +142 -56
  53. package/LICENSE +0 -22
  54. package/sample-cli-config-file.conf +0 -40
  55. 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 `&nbsp` 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
+ }