html-minifier-next 6.2.7 → 6.2.9

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.
@@ -1,5 +1,3 @@
1
- // Imports
2
-
3
1
  import {
4
2
  RE_EVENT_ATTR_DEFAULT,
5
3
  RE_CAN_REMOVE_ATTR_QUOTES,
@@ -21,8 +19,16 @@ import { trimWhitespace, collapseWhitespaceAll } from './whitespace.js';
21
19
  import { shouldMinifyInnerHTML } from './options.js';
22
20
  import { identity, isThenable } from './utils.js';
23
21
 
22
+ // Type definitions
23
+
24
+ /**
25
+ * @typedef {{ name: string, value?: string | undefined, quote?: string, customAssign?: string, customOpen?: string, customClose?: string }} HTMLAttribute
26
+ * Internal counterpart of the public typedef in `htmlminifier.js`—keep in sync.
27
+ */
28
+
24
29
  // Lazy-load entities (used for `decodeEntities` and event-handler attribute decode before `minifyJS`)
25
30
 
31
+ /** @type {Promise<Function> | undefined} */
26
32
  let decodeHTMLStrictPromise;
27
33
  async function getDecodeHTMLStrict() {
28
34
  if (!decodeHTMLStrictPromise) {
@@ -33,20 +39,30 @@ async function getDecodeHTMLStrict() {
33
39
 
34
40
  // Validators
35
41
 
42
+ /**
43
+ * @param {string} text
44
+ * @param {{ignoreCustomComments: RegExp[]}} options
45
+ */
36
46
  function isIgnoredComment(text, options) {
37
- for (let i = 0, len = options.ignoreCustomComments.length; i < len; i++) {
38
- if (options.ignoreCustomComments[i].test(text)) {
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
+ for (const pattern of options.ignoreCustomComments) {
50
+ if (pattern.test(text)) {
39
51
  return true;
40
52
  }
41
53
  }
42
54
  return false;
43
55
  }
44
56
 
57
+ /**
58
+ * @param {string} attrName
59
+ * @param {{customEventAttributes?: RegExp[]}} options
60
+ */
45
61
  function isEventAttribute(attrName, options) {
46
62
  const patterns = options.customEventAttributes;
47
63
  if (patterns) {
48
- for (let i = patterns.length; i--;) {
49
- if (patterns[i].test(attrName)) {
64
+ for (const pattern of patterns) {
65
+ if (pattern.test(attrName)) {
50
66
  return true;
51
67
  }
52
68
  }
@@ -55,14 +71,19 @@ function isEventAttribute(attrName, options) {
55
71
  return RE_EVENT_ATTR_DEFAULT.test(attrName);
56
72
  }
57
73
 
74
+ /** @param {string} value */
58
75
  function canRemoveAttributeQuotes(value) {
59
76
  // https://mathiasbynens.be/notes/unquoted-attribute-values
60
77
  return RE_CAN_REMOVE_ATTR_QUOTES.test(value);
61
78
  }
62
79
 
80
+ /**
81
+ * @param {HTMLAttribute[]} attributes
82
+ * @param {string} attribute
83
+ */
63
84
  function attributesInclude(attributes, attribute) {
64
- for (let i = attributes.length; i--;) {
65
- if (attributes[i].name.toLowerCase() === attribute) {
85
+ for (const attr of attributes) {
86
+ if (attr.name.toLowerCase() === attribute) {
66
87
  return true;
67
88
  }
68
89
  }
@@ -73,9 +94,9 @@ function attributesInclude(attributes, attribute) {
73
94
  * Remove duplicate attributes from an attribute list.
74
95
  * Per HTML spec, when an attribute appears multiple times, the first occurrence wins.
75
96
  * Duplicate attributes result in invalid HTML, so only the first is kept.
76
- * @param {Array} attrs - Array of attribute objects with `name` property
97
+ * @param {HTMLAttribute[]} attrs - Array of attribute objects with `name` property
77
98
  * @param {boolean} caseSensitive - Whether to compare names case-sensitively (for XML/SVG)
78
- * @returns {Array} Deduplicated attribute array (modifies in place and returns)
99
+ * @returns {HTMLAttribute[]} Deduplicated attribute array (modifies in place and returns)
79
100
  */
80
101
  function deduplicateAttributes(attrs, caseSensitive) {
81
102
  if (attrs.length < 2) {
@@ -85,8 +106,7 @@ function deduplicateAttributes(attrs, caseSensitive) {
85
106
  const seen = new Set();
86
107
  let writeIndex = 0;
87
108
 
88
- for (let i = 0; i < attrs.length; i++) {
89
- const attr = attrs[i];
109
+ for (const attr of attrs) {
90
110
  const key = caseSensitive ? attr.name : attr.name.toLowerCase();
91
111
 
92
112
  if (!seen.has(key)) {
@@ -99,6 +119,12 @@ function deduplicateAttributes(attrs, caseSensitive) {
99
119
  return attrs;
100
120
  }
101
121
 
122
+ /**
123
+ * @param {string} tag
124
+ * @param {string} attrName
125
+ * @param {string} attrValue
126
+ * @param {HTMLAttribute[]} attrs
127
+ */
102
128
  function isAttributeRedundant(tag, attrName, attrValue, attrs) {
103
129
  // Fast-path: Check if this element–attribute combination can possibly be redundant
104
130
  // before doing expensive string operations
@@ -132,32 +158,35 @@ function isAttributeRedundant(tag, attrName, attrValue, attrs) {
132
158
  }
133
159
 
134
160
  // Check general defaults
135
- if (hasGeneralDefault && generalDefaults[attrName] === attrValue) {
161
+ if (hasGeneralDefault && /** @type {Record<string, string>} */ (generalDefaults)[attrName] === attrValue) {
136
162
  return true;
137
163
  }
138
164
 
139
165
  // Check tag-specific defaults
140
- return tagHasDefaults && tagDefaults[tag][attrName] === attrValue;
166
+ return tagHasDefaults && /** @type {Record<string, string>} */ (/** @type {Record<string, unknown>} */ (tagDefaults)[tag])[attrName] === attrValue;
141
167
  }
142
168
 
143
169
  function isScriptTypeAttribute(attrValue = '') {
144
- attrValue = trimWhitespace(attrValue.split(/;/, 2)[0]).toLowerCase();
170
+ attrValue = trimWhitespace(attrValue.split(/;/, 2)[0] ?? '').toLowerCase();
145
171
  return attrValue === '' || executableScriptsMimetypes.has(attrValue);
146
172
  }
147
173
 
148
174
  function keepScriptTypeAttribute(attrValue = '') {
149
- attrValue = trimWhitespace(attrValue.split(/;/, 2)[0]).toLowerCase();
175
+ attrValue = trimWhitespace(attrValue.split(/;/, 2)[0] ?? '').toLowerCase();
150
176
  return keepScriptsMimetypes.has(attrValue);
151
177
  }
152
178
 
179
+ /**
180
+ * @param {string} tag
181
+ * @param {HTMLAttribute[]} attrs
182
+ */
153
183
  function isExecutableScript(tag, attrs) {
154
184
  if (tag !== 'script') {
155
185
  return false;
156
186
  }
157
- for (let i = 0, len = attrs.length; i < len; i++) {
158
- const attrName = attrs[i].name.toLowerCase();
159
- if (attrName === 'type') {
160
- return isScriptTypeAttribute(attrs[i].value);
187
+ for (const attr of attrs) {
188
+ if (attr.name.toLowerCase() === 'type') {
189
+ return isScriptTypeAttribute(attr.value);
161
190
  }
162
191
  }
163
192
  return true;
@@ -168,23 +197,30 @@ function isStyleLinkTypeAttribute(attrValue = '') {
168
197
  return attrValue === '' || attrValue === 'text/css';
169
198
  }
170
199
 
200
+ /**
201
+ * @param {string} tag
202
+ * @param {HTMLAttribute[]} attrs
203
+ */
171
204
  function isStyleElement(tag, attrs) {
172
205
  if (tag !== 'style') {
173
206
  return false;
174
207
  }
175
- for (let i = 0, len = attrs.length; i < len; i++) {
176
- const attrName = attrs[i].name.toLowerCase();
177
- if (attrName === 'type') {
178
- return isStyleLinkTypeAttribute(attrs[i].value);
208
+ for (const attr of attrs) {
209
+ if (attr.name.toLowerCase() === 'type') {
210
+ return isStyleLinkTypeAttribute(attr.value);
179
211
  }
180
212
  }
181
213
  return true;
182
214
  }
183
215
 
216
+ /**
217
+ * @param {string} attrName
218
+ * @param {string} attrValue
219
+ */
184
220
  function isBooleanAttribute(attrName, attrValue) {
185
221
  return isSimpleBoolean.has(attrName) ||
186
222
  (attrName === 'draggable' && !isBooleanValue.has(attrValue)) ||
187
- (collapsibleValues.has(attrName) && collapsibleValues.get(attrName).has(attrValue));
223
+ (collapsibleValues.has(attrName) && collapsibleValues.get(attrName)?.has(attrValue));
188
224
  }
189
225
 
190
226
  const uriTypeAttributes = new Map([
@@ -204,6 +240,10 @@ const uriTypeAttributes = new Map([
204
240
  ['script', new Set(['src', 'for'])]
205
241
  ]);
206
242
 
243
+ /**
244
+ * @param {string} attrName
245
+ * @param {string} tag
246
+ */
207
247
  function isUriTypeAttribute(attrName, tag) {
208
248
  const set = uriTypeAttributes.get(tag);
209
249
  return set ? set.has(attrName) : false;
@@ -223,55 +263,87 @@ const numberTypeAttributes = new Map([
223
263
  ['td', new Set(['rowspan', 'colspan'])]
224
264
  ]);
225
265
 
266
+ /**
267
+ * @param {string} attrName
268
+ * @param {string} tag
269
+ */
226
270
  function isNumberTypeAttribute(attrName, tag) {
227
271
  const set = numberTypeAttributes.get(tag);
228
272
  return set ? set.has(attrName) : false;
229
273
  }
230
274
 
275
+ /**
276
+ * @param {string} tag
277
+ * @param {HTMLAttribute[]} attrs
278
+ * @param {string} value
279
+ */
231
280
  function isLinkType(tag, attrs, value) {
232
281
  if (tag !== 'link') return false;
233
282
  const needle = String(value).toLowerCase();
234
- for (let i = 0; i < attrs.length; i++) {
235
- if (attrs[i].name.toLowerCase() === 'rel') {
236
- const tokens = String(attrs[i].value).toLowerCase().split(/\s+/);
283
+ for (const attr of attrs) {
284
+ if (attr.name.toLowerCase() === 'rel') {
285
+ const tokens = String(attr.value).toLowerCase().split(/\s+/);
237
286
  if (tokens.includes(needle)) return true;
238
287
  }
239
288
  }
240
289
  return false;
241
290
  }
242
291
 
292
+ /**
293
+ * @param {string} tag
294
+ * @param {HTMLAttribute[]} attrs
295
+ * @param {string} attrName
296
+ */
243
297
  function isMediaQuery(tag, attrs, attrName) {
244
298
  return attrName === 'media' && (isLinkType(tag, attrs, 'stylesheet') || isStyleElement(tag, attrs));
245
299
  }
246
300
 
301
+ /**
302
+ * @param {string} attrName
303
+ * @param {string} tag
304
+ */
247
305
  function isSrcset(attrName, tag) {
248
306
  return attrName === 'srcset' && srcsetElements.has(tag);
249
307
  }
250
308
 
309
+ /**
310
+ * @param {string} tag
311
+ * @param {HTMLAttribute[]} attrs
312
+ */
251
313
  function isMetaViewport(tag, attrs) {
252
314
  if (tag !== 'meta') {
253
315
  return false;
254
316
  }
255
- for (let i = 0, len = attrs.length; i < len; i++) {
256
- if (attrs[i].name.toLowerCase() === 'name' && attrs[i].value.toLowerCase() === 'viewport') {
317
+ for (const attr of attrs) {
318
+ if (attr.name.toLowerCase() === 'name' && (attr.value || '').toLowerCase() === 'viewport') {
257
319
  return true;
258
320
  }
259
321
  }
260
322
  return false;
261
323
  }
262
324
 
325
+ /**
326
+ * @param {string} tag
327
+ * @param {HTMLAttribute[]} attrs
328
+ */
263
329
  function isContentSecurityPolicy(tag, attrs) {
264
330
  if (tag !== 'meta') {
265
331
  return false;
266
332
  }
267
- for (let i = 0, len = attrs.length; i < len; i++) {
268
- if (attrs[i].name.toLowerCase() === 'http-equiv' && attrs[i].value.toLowerCase() === 'content-security-policy') {
333
+ for (const attr of attrs) {
334
+ if (attr.name.toLowerCase() === 'http-equiv' && (attr.value || '').toLowerCase() === 'content-security-policy') {
269
335
  return true;
270
336
  }
271
337
  }
272
338
  return false;
273
339
  }
274
340
 
341
+ /**
342
+ * @param {string} tag
343
+ * @param {string} attrName
344
+ * @param {string | undefined} attrValue
345
+ * @param {{removeEmptyAttributes?: boolean | Function}} options
346
+ */
275
347
  function canDeleteEmptyAttribute(tag, attrName, attrValue, options) {
276
348
  const isValueEmpty = !attrValue || attrValue.trim() === '';
277
349
  if (!isValueEmpty) {
@@ -283,9 +355,13 @@ function canDeleteEmptyAttribute(tag, attrName, attrValue, options) {
283
355
  return (tag === 'input' && attrName === 'value') || reEmptyAttribute.test(attrName);
284
356
  }
285
357
 
358
+ /**
359
+ * @param {string} name
360
+ * @param {HTMLAttribute[]} attrs
361
+ */
286
362
  function hasAttrName(name, attrs) {
287
- for (let i = attrs.length - 1; i >= 0; i--) {
288
- if (attrs[i].name === name) {
363
+ for (const attr of attrs) {
364
+ if (attr.name === name) {
289
365
  return true;
290
366
  }
291
367
  }
@@ -300,6 +376,14 @@ const valueWhitespaceExemptElements = new Set(['button', 'data', 'input', 'optio
300
376
 
301
377
  // Returns the cleaned attribute value directly (sync) or as a Promise (async);
302
378
  // callers must handle both cases—use `isThenable()` to distinguish
379
+ /**
380
+ * @param {string} tag
381
+ * @param {string} attrName
382
+ * @param {string} attrValue
383
+ * @param {Record<string, any>} options
384
+ * @param {HTMLAttribute[]} attrs
385
+ * @param {Function} minifyHTMLSelf
386
+ */
303
387
  function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTMLSelf) {
304
388
  const isEventAttr = isEventAttribute(attrName, options);
305
389
 
@@ -323,9 +407,9 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
323
407
  return getDecodeHTMLStrict().then(decode => {
324
408
  const decoded = decode(attrValue);
325
409
  const result = options.minifyJS(decoded, true);
326
- const reEncode = v => (v && v.indexOf('&') !== -1) ? v.replace(RE_AMP_ENTITY, '&amp;$1') : v;
410
+ const reEncode = (/** @type {string} */ v) => (v && v.indexOf('&') !== -1) ? v.replace(RE_AMP_ENTITY, '&amp;$1') : v;
327
411
  if (isThenable(result)) {
328
- return result.then(reEncode, err => {
412
+ return result.then(reEncode, (/** @type {Error} */ err) => {
329
413
  if (!options.continueOnMinifyError) throw err;
330
414
  options.log && options.log(err);
331
415
  return attrValue;
@@ -336,7 +420,7 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
336
420
  }
337
421
  const result = options.minifyJS(attrValue, true);
338
422
  if (isThenable(result)) {
339
- return result.catch(err => {
423
+ return result.catch((/** @type {Error} */ err) => {
340
424
  if (!options.continueOnMinifyError) throw err;
341
425
  options.log && options.log(err);
342
426
  return attrValue;
@@ -363,8 +447,8 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
363
447
  const result = options.minifyURLs(attrValue);
364
448
  if (isThenable(result)) {
365
449
  return result
366
- .then(out => typeof out === 'string' ? out : attrValue)
367
- .catch(err => {
450
+ .then((/** @type {unknown} */ out) => typeof out === 'string' ? out : attrValue)
451
+ .catch((/** @type {Error} */ err) => {
368
452
  if (!options.continueOnMinifyError) throw err;
369
453
  options.log && options.log(err);
370
454
  return attrValue;
@@ -387,13 +471,13 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
387
471
  const cssResult = options.minifyCSS(attrValue, 'inline');
388
472
  if (isThenable(cssResult)) {
389
473
  return cssResult
390
- .then(minified => {
474
+ .then((/** @type {string} */ minified) => {
391
475
  // After minification, check if CSS consists entirely of invalid properties (no values)
392
476
  // I.e., `color:` or `margin:;padding:` should be treated as empty
393
477
  if (minified && /^(?:[a-z-]+:[;\s]*)+$/i.test(minified)) return '';
394
478
  return minified;
395
479
  })
396
- .catch(err => {
480
+ .catch((/** @type {Error} */ err) => {
397
481
  if (!options.continueOnMinifyError) throw err;
398
482
  options.log && options.log(err);
399
483
  return originalAttrValue;
@@ -415,8 +499,9 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
415
499
  const match = candidate.match(/\s+([1-9][0-9]*w|[0-9]+(?:\.[0-9]+)?x)$/);
416
500
  if (match) {
417
501
  url = url.slice(0, -match[0].length);
418
- const num = +match[1].slice(0, -1);
419
- const suffix = match[1].slice(-1);
502
+ const group = match[1] ?? '';
503
+ const num = +group.slice(0, -1);
504
+ const suffix = group.slice(-1);
420
505
  if (num !== 1 || suffix !== 'x') {
421
506
  descriptor = ' ' + num + suffix;
422
507
  }
@@ -424,8 +509,8 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
424
509
  const out = options.minifyURLs(url);
425
510
  if (isThenable(out)) {
426
511
  return out
427
- .then(result => (typeof result === 'string' ? result : url) + descriptor)
428
- .catch(err => {
512
+ .then((/** @type {unknown} */ result) => (typeof result === 'string' ? result : url) + descriptor)
513
+ .catch((/** @type {Error} */ err) => {
429
514
  if (!options.continueOnMinifyError) throw err;
430
515
  options.log && options.log(err);
431
516
  return url + descriptor;
@@ -470,7 +555,7 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
470
555
  const originalAttrValue = attrValue;
471
556
  const cssResult = options.minifyCSS(attrValue, 'media');
472
557
  if (isThenable(cssResult)) {
473
- return cssResult.catch(err => {
558
+ return cssResult.catch((/** @type {Error} */ err) => {
474
559
  if (!options.continueOnMinifyError) throw err;
475
560
  options.log && options.log(err);
476
561
  return originalAttrValue;
@@ -494,7 +579,7 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
494
579
  /**
495
580
  * Choose appropriate quote character for an attribute value
496
581
  * @param {string} attrValue - The attribute value
497
- * @param {Object} options - Minifier options
582
+ * @param {{quoteCharacter?: string}} options - Minifier options
498
583
  * @returns {string} The chosen quote character (`"` or `'`)
499
584
  */
500
585
  function chooseAttributeQuote(attrValue, options) {
@@ -513,6 +598,13 @@ function chooseAttributeQuote(attrValue, options) {
513
598
 
514
599
  // Returns the normalized attribute object directly (sync) or as a Promise (async);
515
600
  // callers must handle both cases—use `isThenable()` to distinguish
601
+ /**
602
+ * @param {HTMLAttribute} attr
603
+ * @param {HTMLAttribute[]} attrs
604
+ * @param {string} tag
605
+ * @param {Record<string, any>} options
606
+ * @param {Function} minifyHTML
607
+ */
516
608
  function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
517
609
  const attrName = options.name(attr.name);
518
610
  let attrValue = attr.value;
@@ -528,9 +620,18 @@ function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
528
620
  }
529
621
 
530
622
  // Internal: Handles attribute normalization after entity decoding (if any)
623
+ /**
624
+ * @param {string} attrName
625
+ * @param {string | undefined} attrValue
626
+ * @param {HTMLAttribute} attr
627
+ * @param {HTMLAttribute[]} attrs
628
+ * @param {string} tag
629
+ * @param {Record<string, any>} options
630
+ * @param {Function} minifyHTML
631
+ */
531
632
  function normalizeAttrContinue(attrName, attrValue, attr, attrs, tag, options, minifyHTML) {
532
633
  if ((options.removeRedundantAttributes &&
533
- isAttributeRedundant(tag, attrName, attrValue, attrs)) ||
634
+ isAttributeRedundant(tag, attrName, attrValue ?? '', attrs)) ||
534
635
  (options.removeScriptTypeAttributes && tag === 'script' &&
535
636
  attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue)) ||
536
637
  (options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
@@ -541,7 +642,7 @@ function normalizeAttrContinue(attrName, attrValue, attr, attrs, tag, options, m
541
642
  if (attrValue) {
542
643
  const cleaned = cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTML);
543
644
  if (isThenable(cleaned)) {
544
- return cleaned.then(v => normalizeAttrFinish(attrName, v, attr, tag, options));
645
+ return cleaned.then((/** @type {string | undefined} */ v) => normalizeAttrFinish(attrName, v, attr, tag, options));
545
646
  }
546
647
  return normalizeAttrFinish(attrName, cleaned, attr, tag, options);
547
648
  }
@@ -550,6 +651,13 @@ function normalizeAttrContinue(attrName, attrValue, attr, attrs, tag, options, m
550
651
  }
551
652
 
552
653
  // Internal: Final checks and result assembly after value cleaning
654
+ /**
655
+ * @param {string} attrName
656
+ * @param {string | undefined} attrValue
657
+ * @param {HTMLAttribute} attr
658
+ * @param {string} tag
659
+ * @param {Record<string, any>} options
660
+ */
553
661
  function normalizeAttrFinish(attrName, attrValue, attr, tag, options) {
554
662
  if (options.removeEmptyAttributes &&
555
663
  canDeleteEmptyAttribute(tag, attrName, attrValue, options)) {
@@ -567,6 +675,13 @@ function normalizeAttrFinish(attrName, attrValue, attr, tag, options) {
567
675
  };
568
676
  }
569
677
 
678
+ /**
679
+ * @param {{name: string, value?: string, attr: HTMLAttribute}} normalized
680
+ * @param {string | boolean | undefined} hasUnarySlash
681
+ * @param {Record<string, any>} options
682
+ * @param {boolean} isLast
683
+ * @param {string | undefined} uidAttr
684
+ */
570
685
  function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
571
686
  const attrName = normalized.name;
572
687
  let attrValue = normalized.value;
@@ -578,7 +693,7 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
578
693
  // Determine if need to add/keep quotes
579
694
  const shouldAddQuotes = typeof attrValue !== 'undefined' && (
580
695
  // If `removeAttributeQuotes` is enabled, add quotes only if they can’t be removed
581
- (options.removeAttributeQuotes && (attrValue.indexOf(uidAttr) !== -1 || !canRemoveAttributeQuotes(attrValue))) ||
696
+ (options.removeAttributeQuotes && ((uidAttr ? attrValue.indexOf(uidAttr) !== -1 : false) || !canRemoveAttributeQuotes(attrValue))) ||
582
697
  // If `removeAttributeQuotes` is not enabled, preserve original quote style or add quotes if value requires them
583
698
  (!options.removeAttributeQuotes && (attrQuote !== '' || !canRemoveAttributeQuotes(attrValue) ||
584
699
  // Special case: With `removeTagWhitespace`, unquoted values that aren’t last will have space added,
@@ -587,6 +702,7 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
587
702
  );
588
703
 
589
704
  if (shouldAddQuotes) {
705
+ attrValue = attrValue ?? '';
590
706
  // Determine the appropriate quote character
591
707
  if (!options.preventAttributesEscaping) {
592
708
  // Normal mode: Choose optimal quote type to minimize escaping
@@ -1,5 +1,3 @@
1
- // Imports
2
-
3
1
  import {
4
2
  jsonScriptTypes
5
3
  } from './constants.js';
@@ -9,6 +7,10 @@ import { trimWhitespace } from './whitespace.js';
9
7
 
10
8
  // Wrap CSS declarations for inline styles and media queries
11
9
  // This ensures proper context for CSS minification
10
+ /**
11
+ * @param {string} text
12
+ * @param {string} type
13
+ */
12
14
  function wrapCSS(text, type) {
13
15
  switch (type) {
14
16
  case 'inline':
@@ -20,6 +22,10 @@ function wrapCSS(text, type) {
20
22
  }
21
23
  }
22
24
 
25
+ /**
26
+ * @param {string} text
27
+ * @param {string} type
28
+ */
23
29
  function unwrapCSS(text, type) {
24
30
  let matches;
25
31
  switch (type) {
@@ -30,11 +36,15 @@ function unwrapCSS(text, type) {
30
36
  matches = text.match(/^@media ([\s\S]*?)\s*{[\s\S]*}$/);
31
37
  break;
32
38
  }
33
- return matches ? matches[1] : text;
39
+ return matches ? matches[1] ?? text : text;
34
40
  }
35
41
 
36
42
  // Script processing
37
43
 
44
+ /**
45
+ * @param {string} text
46
+ * @param {{continueOnMinifyError?: boolean, log?: Function}} options
47
+ */
38
48
  function minifyJson(text, options) {
39
49
  try {
40
50
  return JSON.stringify(JSON.parse(text));
@@ -48,11 +58,11 @@ function minifyJson(text, options) {
48
58
  }
49
59
  }
50
60
 
61
+ /** @param {Array<{name: string, value?: string}>} attrs */
51
62
  function hasJsonScriptType(attrs) {
52
- for (let i = 0, len = attrs.length; i < len; i++) {
53
- const attrName = attrs[i].name.toLowerCase();
54
- if (attrName === 'type') {
55
- const attrValue = trimWhitespace((attrs[i].value || '').split(/;/, 2)[0]).toLowerCase();
63
+ for (const attr of attrs) {
64
+ if (attr.name.toLowerCase() === 'type') {
65
+ const attrValue = trimWhitespace((attr.value || '').split(/;/, 2)[0] ?? '').toLowerCase();
56
66
  if (jsonScriptTypes.has(attrValue)) {
57
67
  return true;
58
68
  }
@@ -61,18 +71,24 @@ function hasJsonScriptType(attrs) {
61
71
  return false;
62
72
  }
63
73
 
74
+ /**
75
+ * @param {string} text
76
+ * @param {{continueOnMinifyError?: boolean, log?: Function, processScripts?: string[]}} options
77
+ * @param {Array<{name: string, value?: string | undefined}>} currentAttrs
78
+ * @param {Function} minifyHTML
79
+ */
64
80
  async function processScript(text, options, currentAttrs, minifyHTML) {
65
- for (let i = 0, len = currentAttrs.length; i < len; i++) {
66
- const attrName = currentAttrs[i].name.toLowerCase();
81
+ for (const attr of currentAttrs) {
82
+ const attrName = attr.name.toLowerCase();
67
83
  if (attrName === 'type') {
68
- const rawValue = currentAttrs[i].value;
69
- const normalizedValue = trimWhitespace((rawValue || '').split(/;/, 2)[0]).toLowerCase();
84
+ const rawValue = attr.value;
85
+ const normalizedValue = trimWhitespace((rawValue || '').split(/;/, 2)[0] ?? '').toLowerCase();
70
86
  // Minify JSON script types automatically
71
87
  if (jsonScriptTypes.has(normalizedValue)) {
72
88
  return minifyJson(text, options);
73
89
  }
74
90
  // Process custom script types if specified
75
- if (options.processScripts && options.processScripts.indexOf(rawValue) > -1) {
91
+ if (options.processScripts && rawValue && options.processScripts.indexOf(rawValue) > -1) {
76
92
  return await minifyHTML(text, options);
77
93
  }
78
94
  }
@@ -1,5 +1,3 @@
1
- // Imports
2
-
3
1
  import {
4
2
  headerElements,
5
3
  descriptionElements,
@@ -13,8 +11,20 @@ import {
13
11
  } from './constants.js';
14
12
  import { hasAttrName } from './attributes.js';
15
13
 
14
+ /** @import { HTMLAttribute } from './attributes.js' */
15
+
16
+ // Type definitions
17
+
18
+ /**
19
+ * @typedef {{ name: (str: string) => string, log?: Function }} MinifierOptions
20
+ */
21
+
16
22
  // Tag omission rules
17
23
 
24
+ /**
25
+ * @param {string} optionalStartTag
26
+ * @param {string} tag
27
+ */
18
28
  function canRemoveParentTag(optionalStartTag, tag) {
19
29
  switch (optionalStartTag) {
20
30
  case 'html':
@@ -30,6 +40,10 @@ function canRemoveParentTag(optionalStartTag, tag) {
30
40
  return false;
31
41
  }
32
42
 
43
+ /**
44
+ * @param {string} optionalEndTag
45
+ * @param {string} tag
46
+ */
33
47
  function isStartTagMandatory(optionalEndTag, tag) {
34
48
  switch (tag) {
35
49
  case 'colgroup':
@@ -40,6 +54,10 @@ function isStartTagMandatory(optionalEndTag, tag) {
40
54
  return false;
41
55
  }
42
56
 
57
+ /**
58
+ * @param {string} optionalEndTag
59
+ * @param {string} tag
60
+ */
43
61
  function canRemovePrecedingTag(optionalEndTag, tag) {
44
62
  switch (optionalEndTag) {
45
63
  case 'html':
@@ -79,6 +97,10 @@ function canRemovePrecedingTag(optionalEndTag, tag) {
79
97
 
80
98
  // Element removal logic
81
99
 
100
+ /**
101
+ * @param {string} tag
102
+ * @param {HTMLAttribute[]} attrs
103
+ */
82
104
  function canRemoveElement(tag, attrs) {
83
105
  // Elements with `id` attribute must never be removed—they serve as:
84
106
  // - Navigation targets (skip links, URL fragments)
@@ -146,20 +168,21 @@ function parseElementSpec(str, options) {
146
168
  return null;
147
169
  }
148
170
 
149
- const tag = options.name(match[1]);
150
- const attrString = match[2];
171
+ const tag = options.name(match[1] ?? '');
172
+ const attrString = match[2] ?? '';
151
173
 
152
174
  if (!attrString.trim()) {
153
175
  return { tag, attrs: null };
154
176
  }
155
177
 
156
178
  // Parse attributes from string
179
+ /** @type {{[key: string]: string | undefined}} */
157
180
  const attrs = {};
158
181
  const attrRegex = /([a-zA-Z][\w:-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>/]+)))?/g;
159
182
  let attrMatch;
160
183
 
161
184
  while ((attrMatch = attrRegex.exec(attrString))) {
162
- const attrName = options.name(attrMatch[1]);
185
+ const attrName = options.name(attrMatch[1] ?? '');
163
186
  const attrValue = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4];
164
187
  // Boolean attributes have no value (undefined)
165
188
  attrs[attrName] = attrValue;
@@ -181,7 +204,7 @@ function parseRemoveEmptyElementsExcept(input, options) {
181
204
  return [];
182
205
  }
183
206
 
184
- return input.map(item => {
207
+ return /** @type {Array<{tag: string, attrs: {[x: string]: string | undefined} | null}>} */ (input.map(item => {
185
208
  if (typeof item === 'string') {
186
209
  const spec = parseElementSpec(item, options);
187
210
  if (!spec && options.log) {
@@ -193,7 +216,7 @@ function parseRemoveEmptyElementsExcept(input, options) {
193
216
  options.log('Warning: “removeEmptyElementsExcept” specification must be a string, received: ' + typeof item);
194
217
  }
195
218
  return null;
196
- }).filter(Boolean);
219
+ }).filter(Boolean));
197
220
  }
198
221
 
199
222
  /**