html-minifier-next 6.2.11 → 7.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.
@@ -19,6 +19,8 @@ import { trimWhitespace, collapseWhitespaceAll } from './whitespace.js';
19
19
  import { shouldMinifyInnerHTML } from './options.js';
20
20
  import { identity, isThenable } from './utils.js';
21
21
 
22
+ /** @import { ProcessedOptions } from './options.js' */
23
+
22
24
  // Type definitions
23
25
 
24
26
  /**
@@ -41,11 +43,9 @@ async function getDecodeHTMLStrict() {
41
43
 
42
44
  /**
43
45
  * @param {string} text
44
- * @param {{ignoreCustomComments: RegExp[]}} options
46
+ * @param {ProcessedOptions} options
45
47
  */
46
48
  function isIgnoredComment(text, options) {
47
- // @@ Optimize: `Array.isArray(options.ignoreCustomComments)` runs on every comment node; it could be eliminated once `parseRegExpArray` is tightened to coerce non-arrays to `[]` at setup time
48
- if (!Array.isArray(options.ignoreCustomComments)) return false;
49
49
  for (const pattern of options.ignoreCustomComments) {
50
50
  if (pattern.test(text)) {
51
51
  return true;
@@ -380,7 +380,7 @@ const valueWhitespaceExemptElements = new Set(['button', 'data', 'input', 'optio
380
380
  * @param {string} tag
381
381
  * @param {string} attrName
382
382
  * @param {string} attrValue
383
- * @param {Record<string, any>} options
383
+ * @param {ProcessedOptions} options
384
384
  * @param {HTMLAttribute[]} attrs
385
385
  * @param {Function} minifyHTMLSelf
386
386
  */
@@ -432,7 +432,8 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
432
432
  if (attrName === 'class') {
433
433
  attrValue = trimWhitespace(attrValue);
434
434
  if (options.sortClassNames) {
435
- attrValue = options.sortClassNames(attrValue);
435
+ // By the time attributes are processed, `createSortFns` has replaced any truthy non-function value
436
+ attrValue = /** @type {(value: string) => string} */ (options.sortClassNames)(attrValue);
436
437
  } else {
437
438
  attrValue = collapseWhitespaceAll(attrValue);
438
439
  }
@@ -602,7 +603,7 @@ function chooseAttributeQuote(attrValue, options) {
602
603
  * @param {HTMLAttribute} attr
603
604
  * @param {HTMLAttribute[]} attrs
604
605
  * @param {string} tag
605
- * @param {Record<string, any>} options
606
+ * @param {ProcessedOptions} options
606
607
  * @param {Function} minifyHTML
607
608
  */
608
609
  function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
@@ -626,16 +627,16 @@ function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
626
627
  * @param {HTMLAttribute} attr
627
628
  * @param {HTMLAttribute[]} attrs
628
629
  * @param {string} tag
629
- * @param {Record<string, any>} options
630
+ * @param {ProcessedOptions} options
630
631
  * @param {Function} minifyHTML
631
632
  */
632
633
  function normalizeAttrContinue(attrName, attrValue, attr, attrs, tag, options, minifyHTML) {
633
634
  if ((options.removeRedundantAttributes &&
634
635
  isAttributeRedundant(tag, attrName, attrValue ?? '', attrs)) ||
635
- (options.removeScriptTypeAttributes && tag === 'script' &&
636
- attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue)) ||
637
- (options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
638
- attrName === 'type' && isStyleLinkTypeAttribute(attrValue))) {
636
+ (options.removeDefaultTypeAttributes && attrName === 'type' && (
637
+ ((tag === 'style' || tag === 'link') && isStyleLinkTypeAttribute(attrValue)) ||
638
+ (tag === 'script' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue))
639
+ ))) {
639
640
  return;
640
641
  }
641
642
 
@@ -656,7 +657,7 @@ function normalizeAttrContinue(attrName, attrValue, attr, attrs, tag, options, m
656
657
  * @param {string | undefined} attrValue
657
658
  * @param {HTMLAttribute} attr
658
659
  * @param {string} tag
659
- * @param {Record<string, any>} options
660
+ * @param {ProcessedOptions} options
660
661
  */
661
662
  function normalizeAttrFinish(attrName, attrValue, attr, tag, options) {
662
663
  if (options.removeEmptyAttributes &&
@@ -676,9 +677,9 @@ function normalizeAttrFinish(attrName, attrValue, attr, tag, options) {
676
677
  }
677
678
 
678
679
  /**
679
- * @param {{name: string, value?: string, attr: HTMLAttribute}} normalized
680
+ * @param {{name: string, value: string | undefined, attr: HTMLAttribute}} normalized
680
681
  * @param {string | boolean | undefined} hasUnarySlash
681
- * @param {Record<string, any>} options
682
+ * @param {ProcessedOptions} options
682
683
  * @param {boolean} isLast
683
684
  * @param {string | undefined} uidAttr
684
685
  */
@@ -3,13 +3,15 @@ import {
3
3
  } from './constants.js';
4
4
  import { trimWhitespace } from './whitespace.js';
5
5
 
6
+ /** @import { ProcessedOptions } from './options.js' */
7
+
6
8
  // CSS processing
7
9
 
8
10
  // Wrap CSS declarations for inline styles and media queries
9
11
  // This ensures proper context for CSS minification
10
12
  /**
11
13
  * @param {string} text
12
- * @param {string} type
14
+ * @param {string} [type]
13
15
  */
14
16
  function wrapCSS(text, type) {
15
17
  switch (type) {
@@ -24,7 +26,7 @@ function wrapCSS(text, type) {
24
26
 
25
27
  /**
26
28
  * @param {string} text
27
- * @param {string} type
29
+ * @param {string} [type]
28
30
  */
29
31
  function unwrapCSS(text, type) {
30
32
  let matches;
@@ -73,7 +75,7 @@ function hasJsonScriptType(attrs) {
73
75
 
74
76
  /**
75
77
  * @param {string} text
76
- * @param {{continueOnMinifyError?: boolean, log?: Function, processScripts?: string[]}} options
78
+ * @param {ProcessedOptions} options
77
79
  * @param {Array<{name: string, value?: string | undefined}>} currentAttrs
78
80
  * @param {Function} minifyHTML
79
81
  */
@@ -12,12 +12,7 @@ import {
12
12
  import { hasAttrName } from './attributes.js';
13
13
 
14
14
  /** @import { HTMLAttribute } from './attributes.js' */
15
-
16
- // Type definitions
17
-
18
- /**
19
- * @typedef {{ name: (str: string) => string, log?: Function }} MinifierOptions
20
- */
15
+ /** @import { ProcessedOptions } from './options.js' */
21
16
 
22
17
  // Tag omission rules
23
18
 
@@ -143,7 +138,7 @@ function canRemoveElement(tag, attrs) {
143
138
 
144
139
  /**
145
140
  * @param {string} str - Tag name or HTML-like element spec (e.g., “td” or “<span aria-hidden='true'>”)
146
- * @param {MinifierOptions} options - Options object for name normalization
141
+ * @param {ProcessedOptions} options - Options object for name normalization
147
142
  * @returns {{tag: string, attrs: Object.<string, string|undefined>|null}|null} Parsed spec or null if invalid
148
143
  */
149
144
  function parseElementSpec(str, options) {
@@ -196,7 +191,7 @@ function parseElementSpec(str, options) {
196
191
 
197
192
  /**
198
193
  * @param {string[]} input - Array of element specifications from `removeEmptyElementsExcept` option
199
- * @param {MinifierOptions} options - Options object for parsing
194
+ * @param {ProcessedOptions} options - Options object for parsing
200
195
  * @returns {Array<{tag: string, attrs: Object.<string, string|undefined>|null}>} Array of parsed element specs
201
196
  */
202
197
  function parseRemoveEmptyElementsExcept(input, options) {
@@ -138,6 +138,10 @@ const optionDefinitions = {
138
138
  description: 'Strip HTML comments',
139
139
  type: 'boolean'
140
140
  },
141
+ removeDefaultTypeAttributes: {
142
+ description: 'Remove default `type` attributes from `style`/`link` (e.g., `type="text/css"`) and `script` (e.g., `type="text/javascript"`) elements; other `type` attribute values are left intact',
143
+ type: 'boolean'
144
+ },
141
145
  removeEmptyAttributes: {
142
146
  description: 'Remove all attributes with whitespace-only values',
143
147
  type: 'boolean'
@@ -158,14 +162,6 @@ const optionDefinitions = {
158
162
  description: 'Remove attributes when value matches default',
159
163
  type: 'boolean'
160
164
  },
161
- removeScriptTypeAttributes: {
162
- description: 'Remove `type="text/javascript"` from `script` elements; other `type` attribute values are left intact',
163
- type: 'boolean'
164
- },
165
- removeStyleLinkTypeAttributes: {
166
- description: 'Remove `type="text/css"` from `style` and `link` elements; other `type` attribute values are left intact',
167
- type: 'boolean'
168
- },
169
165
  removeTagWhitespace: {
170
166
  description: 'Remove space between attributes whenever possible; note that this will result in invalid HTML',
171
167
  type: 'boolean'
@@ -4,19 +4,39 @@ import { RE_TRAILING_SEMICOLON } from './constants.js';
4
4
  import { canCollapseWhitespace, canTrimWhitespace } from './whitespace.js';
5
5
  import { wrapCSS, unwrapCSS } from './content.js';
6
6
  import { getPreset, getPresetNames } from '../presets.js';
7
- import { optionDefaults } from './option-definitions.js';
7
+ import { optionDefinitions, optionDefaults } from './option-definitions.js';
8
+
9
+ /** @import { MinifierOptions, HTMLAttribute } from '../htmlminifier.js' */
8
10
 
9
11
  // Type definitions
10
12
 
11
13
  /**
12
- * @typedef {Record<string, any>} MinifierOptions
14
+ * Options object produced by `processOptions` and consumed by `minifyHTML` and
15
+ * the `lib/` helpers; normalization guarantees that the function-valued options
16
+ * below are always present (defaulting to identity/built-in functions), and
17
+ * minification adds writable internal state on top of the public options
18
+ * (set on prototype-chain forks during SVG/MathML namespace transitions)
19
+ *
20
+ * @typedef {Omit<MinifierOptions, 'preset' | 'canCollapseWhitespace' | 'canTrimWhitespace' | 'ignoreCustomComments' | 'log' | 'minifyCSS' | 'minifyJS' | 'minifyURLs' | 'minifySVG'> & {
21
+ * name: (name: string) => string,
22
+ * log: (message: any) => unknown,
23
+ * ignoreCustomComments: RegExp[],
24
+ * canCollapseWhitespace: (tag: string, attrs: HTMLAttribute[], defaultFn: (tag: string) => boolean) => boolean,
25
+ * canTrimWhitespace: (tag: string, attrs: HTMLAttribute[], defaultFn: (tag: string) => boolean) => boolean,
26
+ * minifyCSS: (text: string, type?: string) => string | Promise<string>,
27
+ * minifyJS: (text: string, inline?: boolean, isModule?: boolean) => string | Promise<string>,
28
+ * minifyURLs: (text: string) => string | Promise<string>,
29
+ * minifySVG: ((svgContent: string) => string | Promise<string>) | null,
30
+ * nameParent?: (name: string) => string,
31
+ * nameHTML?: (name: string) => string,
32
+ * insideSVG?: boolean,
33
+ * insideForeignContent?: boolean
34
+ * }} ProcessedOptions
13
35
  */
14
36
 
15
- // @@ Extract a `ProcessedOptions` typedef (with `name`, `log`, `canCollapseWhitespace`, `canTrimWhitespace`,`minifyCSS`, `minifyJS`, `minifyURLs` typed as required, non-optional) to replace the current `Record<string, any>` escape hatch and eliminate the `/** @type {Function} */` casts in htmlminifier.js
16
-
17
37
  // Helper functions
18
38
 
19
- /** @param {MinifierOptions} options */
39
+ /** @param {ProcessedOptions} options */
20
40
  function shouldMinifyInnerHTML(options) {
21
41
  return Boolean(
22
42
  options.collapseWhitespace ||
@@ -29,15 +49,23 @@ function shouldMinifyInnerHTML(options) {
29
49
  );
30
50
  }
31
51
 
52
+ // User-facing option keys that are valid but not listed in `optionDefinitions`
53
+ const optionKeysExtra = new Set(['preset', 'log', 'canCollapseWhitespace', 'canTrimWhitespace', 'cacheCSS', 'cacheJS', 'cacheSVG']);
54
+
55
+ // Unknown option keys and preset names already warned about—warn once per
56
+ // key per process, so repeated `minify` calls (e.g., batch runs) don’t flood STDERR
57
+ const optionKeysWarned = new Set();
58
+ const presetNamesWarned = new Set();
59
+
32
60
  // Main options processor
33
61
 
34
62
  /**
35
63
  * @param {MinifierOptions} inputOptions - User-provided options
36
64
  * @param {{getLightningCSS?: Function | undefined, getTerser?: Function | undefined, getSwc?: Function | undefined, getSvgo?: Function | undefined, cssMinifyCache?: LRU | undefined, jsMinifyCache?: LRU | undefined, svgMinifyCache?: LRU | undefined}} [deps] - Dependencies from htmlminifier.js
37
- * @returns {MinifierOptions} Normalized options with defaults applied
65
+ * @returns {ProcessedOptions} Normalized options with defaults applied
38
66
  */
39
67
  const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getSvgo, cssMinifyCache, jsMinifyCache, svgMinifyCache } = {}) => {
40
- /** @type {MinifierOptions} */
68
+ /** @type {ProcessedOptions} */
41
69
  const options = {
42
70
  name: lowercase,
43
71
  canCollapseWhitespace,
@@ -67,6 +95,18 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
67
95
  });
68
96
  };
69
97
 
98
+ // Route warnings through the user-provided `log` hook so API consumers can
99
+ // capture or suppress them consistently; fall back to `console.warn`
100
+ const warn = typeof inputOptions.log === 'function' ? inputOptions.log : console.warn;
101
+
102
+ // Warn about unrecognized options—catches typos as well as options removed in earlier versions
103
+ Object.keys(inputOptions).forEach(function (key) {
104
+ if (!Object.hasOwn(optionDefinitions, key) && !optionKeysExtra.has(key) && !optionKeysWarned.has(key)) {
105
+ optionKeysWarned.add(key);
106
+ warn(`HTML Minifier Next: Ignoring unknown or deprecated option “${key}” (see README for available options)`);
107
+ }
108
+ });
109
+
70
110
  // Merge preset with user options so all values go through normalization
71
111
  // User options take precedence over preset values
72
112
  let effectiveInput = inputOptions;
@@ -74,17 +114,22 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
74
114
  const preset = getPreset(inputOptions.preset);
75
115
  if (preset) {
76
116
  effectiveInput = { ...preset, ...inputOptions };
77
- } else {
117
+ } else if (!presetNamesWarned.has(inputOptions.preset)) {
118
+ presetNamesWarned.add(inputOptions.preset);
78
119
  const available = getPresetNames().join(', ');
79
- console.warn(`HTML Minifier Next: Unknown preset “${inputOptions.preset}”. Available presets: ${available}`);
120
+ warn(`HTML Minifier Next: Unknown preset “${inputOptions.preset}”; available presets: ${available}`);
80
121
  }
81
122
  }
82
123
 
124
+ // Escape hatch for the loop below, which reads and assigns user-provided values by dynamic key
125
+ const optionsDynamic = /** @type {Record<string, any>} */ (options);
126
+
83
127
  Object.keys(effectiveInput).forEach(function (key) {
84
- const option = effectiveInput[key];
128
+ const option = /** @type {Record<string, any>} */ (effectiveInput)[key];
85
129
 
86
- // Skip preset key—it’s already been processed
87
- if (key === 'preset') {
130
+ // Skip `preset` (already processed) and unrecognized keys (warned about above)—
131
+ // the latter also keeps internal keys from being overridden
132
+ if (key === 'preset' || (!Object.hasOwn(optionDefinitions, key) && !optionKeysExtra.has(key))) {
88
133
  return;
89
134
  }
90
135
 
@@ -106,7 +151,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
106
151
  const cssLoader = getLightningCSS;
107
152
  const cssCache = cssMinifyCache;
108
153
 
109
- options.minifyCSS = async function (/** @type {string} */ text, /** @type {string} */ type) {
154
+ options.minifyCSS = async function (/** @type {string} */ text, /** @type {string | undefined} */ type) {
110
155
  // Fast path: Nothing to minify
111
156
  if (!text || !text.trim()) {
112
157
  return text;
@@ -143,7 +188,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
143
188
  : (inputCSS + '|' + type + '|' + cssSig);
144
189
 
145
190
  try {
146
- const cached = cssCache.get(cssKey);
191
+ const cached = /** @type {string | Promise<string> | undefined} */ (cssCache.get(cssKey));
147
192
  if (cached !== undefined) {
148
193
  // Support both resolved values and in-flight promises
149
194
  return await cached;
@@ -236,7 +281,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
236
281
  cont: !!options.continueOnMinifyError
237
282
  });
238
283
 
239
- options.minifyJS = async function (/** @type {string} */ text, /** @type {boolean} */ inline, /** @type {boolean} */ isModule) {
284
+ options.minifyJS = async function (/** @type {string} */ text, /** @type {boolean | undefined} */ inline, /** @type {boolean | undefined} */ isModule) {
240
285
  const start = text.match(/^\s*<!--.*/);
241
286
  const code = start ? text.slice(start[0].length).replace(/\n\s*-->\s*$/, '') : text;
242
287
 
@@ -258,7 +303,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
258
303
  jsKey = (code.length > 2048 ? (hashContent(code) + '|') : (code + '|'))
259
304
  + (inline ? '1' : '0') + '|' + (isModule ? 'm' : '') + '|' + useEngine + '|' + optsSig;
260
305
 
261
- const cached = jsCache.get(jsKey);
306
+ const cached = /** @type {string | Promise<string> | undefined} */ (jsCache.get(jsKey));
262
307
  if (cached !== undefined) {
263
308
  return await cached;
264
309
  }
@@ -331,7 +376,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
331
376
  }
332
377
 
333
378
  // Check cache
334
- const cached = instanceCache.get(text);
379
+ const cached = /** @type {string | undefined} */ (instanceCache.get(text));
335
380
  if (cached !== undefined) {
336
381
  return cached;
337
382
  }
@@ -377,7 +422,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
377
422
  : (svgContent + '|' + svgSig);
378
423
 
379
424
  try {
380
- const cached = svgCache.get(svgKey);
425
+ const cached = /** @type {string | Promise<string> | undefined} */ (svgCache.get(svgKey));
381
426
  if (cached !== undefined) {
382
427
  return await cached;
383
428
  }
@@ -403,15 +448,15 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
403
448
  };
404
449
  } else if (key === 'customAttrCollapse') {
405
450
  // Single regex pattern
406
- options[key] = parseRegExp(option);
451
+ optionsDynamic[key] = parseRegExp(option);
407
452
  } else if (key === 'customAttrSurround') {
408
453
  // Nested array of RegExp pairs: `[[openRegExp, closeRegExp], …]`
409
- options[key] = parseNestedRegExpArray(option);
454
+ optionsDynamic[key] = parseNestedRegExpArray(option);
410
455
  } else if (['customAttrAssign', 'customEventAttributes', 'ignoreCustomComments', 'ignoreCustomFragments'].includes(key)) {
411
456
  // Array of regex patterns
412
- options[key] = parseRegExpArray(option);
457
+ optionsDynamic[key] = parseRegExpArray(option);
413
458
  } else {
414
- options[key] = option;
459
+ optionsDynamic[key] = option;
415
460
  }
416
461
  });
417
462
  return options;
package/src/lib/utils.js CHANGED
@@ -80,7 +80,10 @@ function identity(value) {
80
80
  return value;
81
81
  }
82
82
 
83
- /** @param {unknown} value */
83
+ /**
84
+ * @param {unknown} value
85
+ * @returns {value is PromiseLike<any>}
86
+ */
84
87
  function isThenable(value) {
85
88
  return value != null && typeof value === 'object' && typeof /** @type {any} */ (value).then === 'function';
86
89
  }
@@ -39,7 +39,7 @@ function collapseWhitespaceAll(str) {
39
39
  // Fast path: No no-break space, common case—just collapse to single space
40
40
  // This avoids the nested regex for the majority of cases
41
41
  if (spaces.indexOf('\xA0') === -1) return ' ';
42
- // For no-break space handling, use the original regex approach
42
+ // For no-break space handling, use the nested regex
43
43
  return spaces.replace(RE_NBSP_LEADING_GROUP, '$1 ');
44
44
  });
45
45
  }
package/src/presets.js CHANGED
@@ -14,8 +14,7 @@ export const presets = {
14
14
  conservativeCollapse: true,
15
15
  preserveLineBreaks: true,
16
16
  removeComments: true,
17
- removeScriptTypeAttributes: true,
18
- removeStyleLinkTypeAttributes: true,
17
+ removeDefaultTypeAttributes: true,
19
18
  useShortDoctype: true
20
19
  },
21
20
  comprehensive: {
@@ -30,11 +29,10 @@ export const presets = {
30
29
  minifyURLs: true,
31
30
  removeAttributeQuotes: true,
32
31
  removeComments: true,
32
+ removeDefaultTypeAttributes: true,
33
33
  removeEmptyAttributes: true,
34
34
  removeOptionalTags: true,
35
35
  removeRedundantAttributes: true,
36
- removeScriptTypeAttributes: true,
37
- removeStyleLinkTypeAttributes: true,
38
36
  useShortDoctype: true
39
37
  }
40
38
  };