html-minifier-next 6.2.10 → 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.
@@ -54,6 +54,8 @@ import {
54
54
 
55
55
  import { processOptions } from './lib/options.js';
56
56
 
57
+ /** @import { ProcessedOptions } from './lib/options.js' */
58
+
57
59
  // Type definitions
58
60
 
59
61
  /**
@@ -171,7 +173,7 @@ import { processOptions } from './lib/options.js';
171
173
  *
172
174
  * @prop {RegExp[]} [customAttrAssign]
173
175
  * Array of regexes used to recognise custom attribute assignment
174
- * operators (e.g. `'<div flex?="{{mode != cover}}"></div>'`).
176
+ * operators (e.g., `'<div flex?="{{mode != cover}}"></div>'`).
175
177
  * These are concatenated with the built-in assignment patterns.
176
178
  *
177
179
  * Default: `[]`
@@ -179,16 +181,16 @@ import { processOptions } from './lib/options.js';
179
181
  * @prop {RegExp} [customAttrCollapse]
180
182
  * Regex matching attribute names whose values should be collapsed.
181
183
  * Basically used to remove newlines and excess spaces inside attribute values,
182
- * e.g. `/ng-class/`.
184
+ * e.g., `/ng-class/`.
183
185
  *
184
186
  * @prop {[RegExp, RegExp][]} [customAttrSurround]
185
187
  * Array of `[openRegExp, closeRegExp]` pairs used by the parser to
186
188
  * detect custom attribute surround patterns (for non-standard syntaxes,
187
- * e.g. `<input {{#if value}}checked="checked"{{/if}}>`).
189
+ * e.g., `<input {{#if value}}checked="checked"{{/if}}>`).
188
190
  *
189
191
  * @prop {RegExp[]} [customEventAttributes]
190
192
  * Array of regexes used to detect event handler attributes for `minifyJS`
191
- * (e.g. `ng-click`). The default matches standard `on…` event attributes.
193
+ * (e.g., `ng-click`). The default matches standard `on…` event attributes.
192
194
  *
193
195
  * Default: `[/^on[a-z]{3,}$/]`
194
196
  *
@@ -303,13 +305,6 @@ import { processOptions } from './lib/options.js';
303
305
  *
304
306
  * Default: `false`
305
307
  *
306
- * @prop {(name: string) => string} [name]
307
- * Function used to normalise tag/attribute names. By default, this lowercases
308
- * names, unless `caseSensitive` is enabled.
309
- *
310
- * Default: `(name) => name.toLowerCase()`,
311
- * or `(name) => name` (no-op function) if `caseSensitive` is enabled.
312
- *
313
308
  * @prop {boolean} [noNewlinesBeforeTagClose]
314
309
  * When wrapping lines, prevent inserting a newline directly before a
315
310
  * closing tag (useful to keep tags like `</a>` on the same line).
@@ -332,6 +327,13 @@ import { processOptions } from './lib/options.js';
332
327
  *
333
328
  * Default: `false`
334
329
  *
330
+ * @prop {string} [preset]
331
+ * Name of a preset configuration to use as a base (`conservative` or
332
+ * `comprehensive`). Explicitly provided options take precedence over
333
+ * preset values. Unknown preset names are ignored (with a warning).
334
+ *
335
+ * Default: No preset
336
+ *
335
337
  * @prop {boolean} [preventAttributesEscaping]
336
338
  * When true, attribute values will not be HTML-escaped (dangerous for
337
339
  * untrusted input). By default, attributes are escaped.
@@ -341,7 +343,7 @@ import { processOptions } from './lib/options.js';
341
343
  * @prop {string[]} [processScripts]
342
344
  * Array of `type` attribute values for `<script>` elements whose contents
343
345
  * should be processed as HTML
344
- * (e.g. `text/ng-template`, `text/x-handlebars-template`, etc.).
346
+ * (e.g., `text/ng-template`, `text/x-handlebars-template`, etc.).
345
347
  * When present, the contents of matching script tags are recursively minified,
346
348
  * like normal HTML content.
347
349
  *
@@ -366,6 +368,12 @@ import { processOptions } from './lib/options.js';
366
368
  *
367
369
  * Default: `false`
368
370
  *
371
+ * @prop {boolean} [removeDefaultTypeAttributes]
372
+ * Remove default `type` attributes from `<style>`/`<link>` (e.g., `type="text/css"`)
373
+ * and `<script>` (e.g., `type="text/javascript"`) elements.
374
+ *
375
+ * Default: `false`
376
+ *
369
377
  * @prop {boolean | ((attrName: string, tag: string) => boolean)} [removeEmptyAttributes]
370
378
  * If true, removes attributes whose values are empty (some attributes
371
379
  * are excluded by name). Can also be a function to customise which empty
@@ -420,18 +428,6 @@ import { processOptions } from './lib/options.js';
420
428
  *
421
429
  * Default: `false`
422
430
  *
423
- * @prop {boolean} [removeScriptTypeAttributes]
424
- * Remove `type` attributes from `<script>` when they are unnecessary
425
- * (e.g. `type="text/javascript"`).
426
- *
427
- * Default: `false`
428
- *
429
- * @prop {boolean} [removeStyleLinkTypeAttributes]
430
- * Remove `type` attributes from `<style>` and `<link>` elements when
431
- * they are unnecessary (e.g. `type="text/css"`).
432
- *
433
- * Default: `false`
434
- *
435
431
  * @prop {boolean} [removeTagWhitespace]
436
432
  * **Note that this will result in invalid HTML!**
437
433
  *
@@ -469,11 +465,6 @@ import { processOptions } from './lib/options.js';
469
465
  * See also: https://perfectionkills.com/experimenting-with-html-minifier/#use_short_doctype
470
466
  *
471
467
  * Default: `false`
472
- *
473
- * @prop {boolean} [insideSVG] - Internal: Set when inside SVG/MathML context
474
- * @prop {boolean} [insideForeignContent] - Internal: Set when inside SVG `foreignObject`
475
- * @prop {Function} [parentName] - Internal: Preserved name function during namespace transitions
476
- * @prop {Function} [htmlName] - Internal: HTML name function preserved from outer context
477
468
  */
478
469
 
479
470
  // Lazy-load heavy dependencies only when needed
@@ -717,7 +708,7 @@ function mergeConsecutiveScripts(html) {
717
708
 
718
709
  /**
719
710
  * @param {string} value
720
- * @param {MinifierOptions} options
711
+ * @param {ProcessedOptions} options
721
712
  * @param {string} uidIgnore
722
713
  * @param {string} uidAttr
723
714
  * @param {string[]} ignoredMarkupChunks
@@ -725,7 +716,7 @@ function mergeConsecutiveScripts(html) {
725
716
  async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupChunks) {
726
717
  const attrChains = options.sortAttributes && typeof options.sortAttributes !== 'function' && Object.create(null);
727
718
  const classChain = options.sortClassNames && typeof options.sortClassNames !== 'function' && new TokenChain();
728
- const resolveName = /** @type {(name: string) => string} */ (options.name);
719
+ const resolveName = options.name;
729
720
 
730
721
  function attrNames(/** @type {HTMLAttribute[]} */ attrs) {
731
722
  return attrs.map(function (attr) {
@@ -804,7 +795,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
804
795
  }
805
796
 
806
797
  // For the first pass, create a copy of options and disable aggressive minification.
807
- // Keep attribute transformations (like `removeStyleLinkTypeAttributes`) for accurate analysis.
798
+ // Keep attribute transformations (like `removeDefaultTypeAttributes`) for accurate analysis.
808
799
  // This is safe because `createSortFns` is called before custom fragment UID markers (`uidAttr`) are added.
809
800
  // Note: `htmlmin:ignore` UID markers (`uidIgnore`) already exist and are expanded for analysis.
810
801
  const firstPassOptions = Object.assign({}, options, {
@@ -818,7 +809,6 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
818
809
  decodeEntities: false,
819
810
  processScripts: false,
820
811
  // Keep `ignoreCustomFragments` to handle template syntax correctly
821
- // This is safe because `createSortFns` is now called before UID markers are added
822
812
  // Continue on parse errors during analysis (e.g., template syntax)
823
813
  continueOnParseError: true,
824
814
  log: identity
@@ -848,7 +838,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
848
838
  uidReplacePattern.lastIndex = 0;
849
839
  }
850
840
 
851
- // First pass minification applies attribute transformations like `removeStyleLinkTypeAttributes` for accurate frequency analysis
841
+ // First pass minification applies attribute transformations like `removeDefaultTypeAttributes` for accurate frequency analysis
852
842
  const firstPassOutput = await minifyHTML(expandedValue, firstPassOptions);
853
843
 
854
844
  // For frequency analysis, remove custom fragments temporarily
@@ -944,7 +934,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
944
934
 
945
935
  /**
946
936
  * @param {string} value - HTML content to minify
947
- * @param {MinifierOptions} options - Normalized minification options
937
+ * @param {ProcessedOptions} options - Normalized minification options
948
938
  * @param {boolean} [partialMarkup] - Whether treating input as partial markup
949
939
  * @returns {Promise<string>} Minified HTML
950
940
  */
@@ -958,7 +948,7 @@ async function minifyHTML(value, options, partialMarkup) {
958
948
  value = collapseWhitespace(value, options, true, true);
959
949
  }
960
950
 
961
- const resolveName = /** @type {(name: string) => string} */ (options.name);
951
+ const resolveName = options.name;
962
952
  /** @type {HTMLAttribute[]} */
963
953
  const emptyAttrs = [];
964
954
 
@@ -1015,7 +1005,7 @@ async function minifyHTML(value, options, partialMarkup) {
1015
1005
  }
1016
1006
  removeEmptyElementsExcept = [];
1017
1007
  } else {
1018
- removeEmptyElementsExcept = parseRemoveEmptyElementsExcept(options.removeEmptyElementsExcept || [], /** @type {any} */ (options)) || [];
1008
+ removeEmptyElementsExcept = parseRemoveEmptyElementsExcept(options.removeEmptyElementsExcept || [], options) || [];
1019
1009
  }
1020
1010
 
1021
1011
  // Temporarily replace ignored chunks with comments, so that there’s no need to worry what’s there;
@@ -1096,8 +1086,8 @@ async function minifyHTML(value, options, partialMarkup) {
1096
1086
  uidAttrLeadingPattern = new RegExp('^\\s*' + uidAttr + '(\\d+)' + uidAttr);
1097
1087
 
1098
1088
  if (options.minifyCSS !== identity) {
1099
- /** @type {any} */ (options).minifyCSS = (function (/** @type {Function} */ fn) {
1100
- return function (/** @type {string} */ text, /** @type {string} */ type) {
1089
+ options.minifyCSS = (function (/** @type {ProcessedOptions['minifyCSS']} */ fn) {
1090
+ return function (/** @type {string} */ text, /** @type {string | undefined} */ type) {
1101
1091
  text = text.replace(/** @type {RegExp} */ (uidPattern), function (/** @type {string} */ _match, /** @type {string} */ _prefix, /** @type {string} */ index) {
1102
1092
  const chunks = ignoredCustomMarkupChunks[+index];
1103
1093
  return (chunks?.[1] ?? '') + uidAttr + index + uidAttr + (chunks?.[2] ?? '');
@@ -1105,18 +1095,18 @@ async function minifyHTML(value, options, partialMarkup) {
1105
1095
 
1106
1096
  return fn(text, type);
1107
1097
  };
1108
- })(/** @type {Function} */ (options.minifyCSS));
1098
+ })(options.minifyCSS);
1109
1099
  }
1110
1100
 
1111
1101
  if (options.minifyJS !== identity) {
1112
- options.minifyJS = (function (/** @type {Function} */ fn) {
1113
- return function (/** @type {string} */ text, /** @type {boolean} */ inline, /** @type {boolean} */ isModule) {
1102
+ options.minifyJS = (function (/** @type {ProcessedOptions['minifyJS']} */ fn) {
1103
+ return function (/** @type {string} */ text, /** @type {boolean | undefined} */ inline, /** @type {boolean | undefined} */ isModule) {
1114
1104
  return fn(text.replace(/** @type {RegExp} */ (uidPattern), function (/** @type {string} */ _match, /** @type {string} */ _prefix, /** @type {string} */ index) {
1115
1105
  const chunks = ignoredCustomMarkupChunks[+index];
1116
1106
  return (chunks?.[1] ?? '') + uidAttr + index + uidAttr + (chunks?.[2] ?? '');
1117
1107
  }), inline, isModule);
1118
1108
  };
1119
- })(/** @type {Function} */ (options.minifyJS));
1109
+ })(options.minifyJS);
1120
1110
  }
1121
1111
  }
1122
1112
 
@@ -1128,11 +1118,11 @@ async function minifyHTML(value, options, partialMarkup) {
1128
1118
  }
1129
1119
 
1130
1120
  function canCollapseWhitespace(/** @type {string} */ tag, /** @type {HTMLAttribute[]} */ attrs) {
1131
- return /** @type {Function} */ (options.canCollapseWhitespace)(tag, attrs, defaultCanCollapseWhitespace);
1121
+ return options.canCollapseWhitespace(tag, attrs, defaultCanCollapseWhitespace);
1132
1122
  }
1133
1123
 
1134
1124
  function canTrimWhitespace(/** @type {string} */ tag, /** @type {HTMLAttribute[]} */ attrs) {
1135
- return /** @type {Function} */ (options.canTrimWhitespace)(tag, attrs, defaultCanTrimWhitespace);
1125
+ return options.canTrimWhitespace(tag, attrs, defaultCanTrimWhitespace);
1136
1126
  }
1137
1127
 
1138
1128
  function removeStartTag() {
@@ -1195,10 +1185,14 @@ async function minifyHTML(value, options, partialMarkup) {
1195
1185
  start: async function (/** @type {string} */ tag, /** @type {HTMLAttribute[]} */ attrs, /** @type {boolean} */ unary, /** @type {string} */ unarySlash, /** @type {boolean} */ autoGenerated) {
1196
1186
  const lowerTag = tag.toLowerCase();
1197
1187
  if (lowerTag === 'svg' || lowerTag === 'math') {
1188
+ // Preserve the surrounding HTML context’s name function (e.g., `identity`
1189
+ // under `caseSensitive`) so `foreignObject`/`annotation-xml` can restore it
1190
+ const nameHTML = options.name;
1198
1191
  options = Object.create(options);
1199
1192
  options.caseSensitive = true;
1200
1193
  options.keepClosingSlash = true;
1201
1194
  options.name = identity;
1195
+ options.nameHTML = nameHTML;
1202
1196
  options.insideSVG = lowerTag === 'svg';
1203
1197
  options.insideForeignContent = true;
1204
1198
  // Disable HTML-specific options that produce invalid XML:
@@ -1212,23 +1206,24 @@ async function minifyHTML(value, options, partialMarkup) {
1212
1206
  }
1213
1207
  // `foreignObject` in SVG and `annotation-xml` in MathML contain HTML content
1214
1208
  // Note: The element itself is in SVG/MathML namespace, only its children are HTML
1215
- let useParentNameForTag = false;
1209
+ let useNameParentForTag = false;
1216
1210
  if (options.insideForeignContent && (lowerTag === 'foreignobject' ||
1217
1211
  (lowerTag === 'annotation-xml' && attrs.some((/** @type {HTMLAttribute} */ a) => a.name.toLowerCase() === 'encoding' &&
1218
1212
  RE_HTML_ENCODING.test(a.value ?? ''))))) {
1219
- const parentName = /** @type {(name: string) => string} */ (options.name);
1213
+ const nameParent = options.name;
1220
1214
  options = Object.create(options);
1221
1215
  options.caseSensitive = false;
1222
1216
  options.keepClosingSlash = false;
1223
- options.parentName = parentName; // Preserve for the element tag itself
1224
- options.name = /** @type {(name: string) => string} */ (options.htmlName ?? lowercase);
1217
+ options.nameParent = nameParent; // Preserve for the element tag itself
1218
+ options.name = options.nameHTML ?? lowercase;
1225
1219
  options.insideForeignContent = false;
1226
1220
  // Note: `removeAttributeQuotes`, `removeTagWhitespace`, and `decodeEntities`
1227
1221
  // stay disabled (inherited from SVG context) because the entire SVG block
1228
1222
  // must be valid XML for SVGO processing
1229
- useParentNameForTag = true;
1223
+ useNameParentForTag = true;
1230
1224
  }
1231
- tag = /** @type {(name: string) => string} */ (useParentNameForTag ? options.parentName : options.name)(tag);
1225
+ // `nameParent` is always set when `useNameParentForTag` is true; the extra check only narrows the type
1226
+ tag = (useNameParentForTag && options.nameParent ? options.nameParent : options.name)(tag);
1232
1227
  currentTag = tag;
1233
1228
  charsPrevTag = tag;
1234
1229
  if (!inlineTextSet.has(tag)) {
@@ -1297,16 +1292,19 @@ async function minifyHTML(value, options, partialMarkup) {
1297
1292
  deduplicateAttributes(attrs, Boolean(options.caseSensitive));
1298
1293
 
1299
1294
  if (options.sortAttributes) {
1300
- /** @type {Function} */ (options.sortAttributes)(tag, attrs);
1295
+ // By the time tags are processed, `createSortFns` has replaced any truthy non-function value
1296
+ /** @type {(tag: string, attrs: HTMLAttribute[]) => void} */ (options.sortAttributes)(tag, attrs);
1301
1297
  }
1302
1298
 
1303
1299
  const attrResults = attrs.map(attr => normalizeAttr(attr, attrs, tag, options, minifyHTML));
1304
- const normalizedAttrs = attrResults.some(isThenable) ? await Promise.all(attrResults) : attrResults;
1300
+ // The `isThenable` probe guarantees the sync branch holds no promises
1301
+ const normalizedAttrs = /** @type {Array<{name: string, value: string | undefined, attr: HTMLAttribute} | undefined>} */ (attrResults.some(isThenable) ? await Promise.all(attrResults) : attrResults);
1305
1302
  const parts = [];
1306
1303
  let isLast = true;
1307
1304
  for (let i = normalizedAttrs.length - 1; i >= 0; i--) {
1308
- if (normalizedAttrs[i]) {
1309
- parts.push(buildAttr(normalizedAttrs[i], hasUnarySlash, options, isLast, uidAttr));
1305
+ const normalizedAttr = normalizedAttrs[i];
1306
+ if (normalizedAttr) {
1307
+ parts.push(buildAttr(normalizedAttr, hasUnarySlash, options, isLast, uidAttr));
1310
1308
  isLast = false;
1311
1309
  }
1312
1310
  }
@@ -1336,7 +1334,7 @@ async function minifyHTML(value, options, partialMarkup) {
1336
1334
  !options.insideForeignContent && Object.getPrototypeOf(options).insideForeignContent) {
1337
1335
  options = Object.getPrototypeOf(options);
1338
1336
  }
1339
- tag = /** @type {Function} */ (options.name)(tag);
1337
+ tag = options.name(tag);
1340
1338
 
1341
1339
  // Check if current tag is in a whitespace stack
1342
1340
  if (options.collapseWhitespace) {
@@ -1604,10 +1602,10 @@ async function minifyHTML(value, options, partialMarkup) {
1604
1602
  text = await processScript(text, options, currentAttrs, minifyHTML);
1605
1603
  }
1606
1604
  if (needsMinifyJS) {
1607
- text = await /** @type {Function} */ (options.minifyJS)(text, false, isModuleScript);
1605
+ text = await options.minifyJS(text, false, isModuleScript);
1608
1606
  }
1609
1607
  if (needsMinifyCSS) {
1610
- text = await /** @type {Function} */ (options.minifyCSS)(text);
1608
+ text = await options.minifyCSS(text);
1611
1609
  }
1612
1610
  charsFinalize(text);
1613
1611
  })();
@@ -1724,7 +1722,7 @@ async function minifyHTML(value, options, partialMarkup) {
1724
1722
  }
1725
1723
 
1726
1724
  if (options.removeComments) {
1727
- if (isIgnoredComment(text, /** @type {any} */ (options))) {
1725
+ if (isIgnoredComment(text, options)) {
1728
1726
  text = prefix + text + suffix;
1729
1727
  } else {
1730
1728
  text = '';
@@ -1746,10 +1744,11 @@ async function minifyHTML(value, options, partialMarkup) {
1746
1744
 
1747
1745
  // Post-processing: Optimize SVG blocks with SVGO
1748
1746
  // Run all SVGO calls in parallel, then splice results in reverse to preserve indices
1749
- if (options.minifySVG && svgBlocks.length) {
1747
+ const minifySVG = options.minifySVG;
1748
+ if (minifySVG && svgBlocks.length) {
1750
1749
  const optimized = await Promise.all(
1751
1750
  svgBlocks.map(({ start, end }) =>
1752
- /** @type {Function} */ (options.minifySVG)(buffer.slice(start, end).join(''))
1751
+ minifySVG(buffer.slice(start, end).join(''))
1753
1752
  )
1754
1753
  );
1755
1754
  for (let i = svgBlocks.length - 1; i >= 0; i--) {
@@ -1803,7 +1802,7 @@ async function minifyHTML(value, options, partialMarkup) {
1803
1802
 
1804
1803
  /**
1805
1804
  * @param {string[]} results
1806
- * @param {MinifierOptions} options
1805
+ * @param {ProcessedOptions} options
1807
1806
  * @param {Function} restoreCustom
1808
1807
  * @param {Function} restoreIgnore
1809
1808
  */
@@ -1905,7 +1904,7 @@ export const minify = async function (value, options) {
1905
1904
  // Initialize caches on first use with configurable sizes
1906
1905
  const caches = initCaches(options || {});
1907
1906
 
1908
- options = processOptions(options || {}, {
1907
+ const processedOptions = processOptions(options || {}, {
1909
1908
  getLightningCSS,
1910
1909
  getTerser,
1911
1910
  getSwc,
@@ -1914,14 +1913,14 @@ export const minify = async function (value, options) {
1914
1913
  jsMinifyCache: caches.jsMinifyCache ?? undefined,
1915
1914
  svgMinifyCache: caches.svgMinifyCache ?? undefined
1916
1915
  });
1917
- let result = await minifyHTML(value, options);
1916
+ let result = await minifyHTML(value, processedOptions);
1918
1917
 
1919
1918
  // Post-processing: Merge consecutive inline scripts if enabled
1920
- if (options.mergeScripts) {
1919
+ if (processedOptions.mergeScripts) {
1921
1920
  result = mergeConsecutiveScripts(result);
1922
1921
  }
1923
1922
 
1924
- /** @type {Function} */ (options.log)('minified in: ' + (Date.now() - start) + 'ms');
1923
+ processedOptions.log('minified in: ' + (Date.now() - start) + 'ms');
1925
1924
  return result;
1926
1925
  };
1927
1926
 
package/src/htmlparser.js CHANGED
@@ -428,7 +428,7 @@ export class HTMLParser {
428
428
  advance(m[0].length);
429
429
  await parseEndTag('</' + stackedTag + '>', stackedTag);
430
430
  } else {
431
- // No closing tag found; to avoid infinite loop, break similarly to previous behavior
431
+ // No closing tag found; break to avoid an infinite loop
432
432
  if (handler.continueOnParseError && handler.chars && pos < fullLength) {
433
433
  const result = handler.chars(fullHtml[pos], prevTag, '', prevAttrs, []);
434
434
  if (isThenable(result)) await result;
@@ -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'