html-minifier-next 4.16.4 → 4.17.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.
@@ -5,7 +5,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  var entities = require('entities');
6
6
  var RelateURL = require('relateurl');
7
7
 
8
- /*!
8
+ /*
9
9
  * HTML Parser By John Resig (ejohn.org)
10
10
  * Modified by Juriy “kangax” Zaytsev
11
11
  * Original code by Erik Arvidsson, Mozilla Public License
@@ -16,10 +16,10 @@ var RelateURL = require('relateurl');
16
16
  * Use like so:
17
17
  *
18
18
  * HTMLParser(htmlString, {
19
- * start: function(tag, attrs, unary) {},
20
- * end: function(tag) {},
21
- * chars: function(text) {},
22
- * comment: function(text) {}
19
+ * start: function(tag, attrs, unary) {},
20
+ * end: function(tag) {},
21
+ * chars: function(text) {},
22
+ * comment: function(text) {}
23
23
  * });
24
24
  */
25
25
 
@@ -42,7 +42,7 @@ const singleAttrValues = [
42
42
  ];
43
43
  // https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
44
44
  const qnameCapture = (function () {
45
- // based on https://www.npmjs.com/package/ncname
45
+ // https://www.npmjs.com/package/ncname
46
46
  const combiningChar = '\\u0300-\\u0345\\u0360\\u0361\\u0483-\\u0486\\u0591-\\u05A1\\u05A3-\\u05B9\\u05BB-\\u05BD\\u05BF\\u05C1\\u05C2\\u05C4\\u064B-\\u0652\\u0670\\u06D6-\\u06E4\\u06E7\\u06E8\\u06EA-\\u06ED\\u0901-\\u0903\\u093C\\u093E-\\u094D\\u0951-\\u0954\\u0962\\u0963\\u0981-\\u0983\\u09BC\\u09BE-\\u09C4\\u09C7\\u09C8\\u09CB-\\u09CD\\u09D7\\u09E2\\u09E3\\u0A02\\u0A3C\\u0A3E-\\u0A42\\u0A47\\u0A48\\u0A4B-\\u0A4D\\u0A70\\u0A71\\u0A81-\\u0A83\\u0ABC\\u0ABE-\\u0AC5\\u0AC7-\\u0AC9\\u0ACB-\\u0ACD\\u0B01-\\u0B03\\u0B3C\\u0B3E-\\u0B43\\u0B47\\u0B48\\u0B4B-\\u0B4D\\u0B56\\u0B57\\u0B82\\u0B83\\u0BBE-\\u0BC2\\u0BC6-\\u0BC8\\u0BCA-\\u0BCD\\u0BD7\\u0C01-\\u0C03\\u0C3E-\\u0C44\\u0C46-\\u0C48\\u0C4A-\\u0C4D\\u0C55\\u0C56\\u0C82\\u0C83\\u0CBE-\\u0CC4\\u0CC6-\\u0CC8\\u0CCA-\\u0CCD\\u0CD5\\u0CD6\\u0D02\\u0D03\\u0D3E-\\u0D43\\u0D46-\\u0D48\\u0D4A-\\u0D4D\\u0D57\\u0E31\\u0E34-\\u0E3A\\u0E47-\\u0E4E\\u0EB1\\u0EB4-\\u0EB9\\u0EBB\\u0EBC\\u0EC8-\\u0ECD\\u0F18\\u0F19\\u0F35\\u0F37\\u0F39\\u0F3E\\u0F3F\\u0F71-\\u0F84\\u0F86-\\u0F8B\\u0F90-\\u0F95\\u0F97\\u0F99-\\u0FAD\\u0FB1-\\u0FB7\\u0FB9\\u20D0-\\u20DC\\u20E1\\u302A-\\u302F\\u3099\\u309A';
47
47
  const digit = '0-9\\u0660-\\u0669\\u06F0-\\u06F9\\u0966-\\u096F\\u09E6-\\u09EF\\u0A66-\\u0A6F\\u0AE6-\\u0AEF\\u0B66-\\u0B6F\\u0BE7-\\u0BEF\\u0C66-\\u0C6F\\u0CE6-\\u0CEF\\u0D66-\\u0D6F\\u0E50-\\u0E59\\u0ED0-\\u0ED9\\u0F20-\\u0F29';
48
48
  const extender = '\\xB7\\u02D0\\u02D1\\u0387\\u0640\\u0E46\\u0EC6\\u3005\\u3031-\\u3035\\u309D\\u309E\\u30FC-\\u30FE';
@@ -82,7 +82,7 @@ const nonPhrasing = new CaseInsensitiveSet(['address', 'article', 'aside', 'base
82
82
  const reCache = {};
83
83
 
84
84
  // Pre-compiled regexes for common special elements (`script`, `style`, `noscript`)
85
- // These are used frequently and pre-compiling them avoids regex creation overhead
85
+ // These are used frequently, and pre-compiling them avoids regex creation overhead
86
86
  const preCompiledStackedTags = {
87
87
  'script': /([\s\S]*?)<\/script[^>]*>/i,
88
88
  'style': /([\s\S]*?)<\/style[^>]*>/i,
@@ -145,6 +145,7 @@ class HTMLParser {
145
145
  // Use cached attribute regex for this handler configuration
146
146
  const attribute = getAttrRegexForHandler(handler);
147
147
  let prevTag = undefined, nextTag = undefined;
148
+ let prevAttrs = [], nextAttrs = [];
148
149
 
149
150
  // Index-based parsing
150
151
  let pos = 0;
@@ -188,6 +189,7 @@ class HTMLParser {
188
189
  }
189
190
  advance(commentEnd + 3);
190
191
  prevTag = '';
192
+ prevAttrs = [];
191
193
  continue;
192
194
  }
193
195
  }
@@ -202,6 +204,7 @@ class HTMLParser {
202
204
  }
203
205
  advance(conditionalEnd + 2);
204
206
  prevTag = '';
207
+ prevAttrs = [];
205
208
  continue;
206
209
  }
207
210
  }
@@ -214,6 +217,7 @@ class HTMLParser {
214
217
  }
215
218
  advance(doctypeMatch[0].length);
216
219
  prevTag = '';
220
+ prevAttrs = [];
217
221
  continue;
218
222
  }
219
223
 
@@ -223,6 +227,7 @@ class HTMLParser {
223
227
  advance(endTagMatch[0].length);
224
228
  await parseEndTag(endTagMatch[0], endTagMatch[1]);
225
229
  prevTag = '/' + endTagMatch[1].toLowerCase();
230
+ prevAttrs = [];
226
231
  continue;
227
232
  }
228
233
 
@@ -255,19 +260,24 @@ class HTMLParser {
255
260
  let nextTagMatch = parseStartTag(nextHtml);
256
261
  if (nextTagMatch) {
257
262
  nextTag = nextTagMatch.tagName;
263
+ // Extract minimal attribute info for whitespace logic (just name/value pairs)
264
+ nextAttrs = extractAttrInfo(nextTagMatch.attrs);
258
265
  } else {
259
266
  nextTagMatch = nextHtml.match(endTag);
260
267
  if (nextTagMatch) {
261
268
  nextTag = '/' + nextTagMatch[1];
269
+ nextAttrs = [];
262
270
  } else {
263
271
  nextTag = '';
272
+ nextAttrs = [];
264
273
  }
265
274
  }
266
275
 
267
276
  if (handler.chars) {
268
- await handler.chars(text, prevTag, nextTag);
277
+ await handler.chars(text, prevTag, nextTag, prevAttrs, nextAttrs);
269
278
  }
270
279
  prevTag = '';
280
+ prevAttrs = [];
271
281
  } else {
272
282
  const stackedTag = lastTag.toLowerCase();
273
283
  // Use pre-compiled regex for common tags (`script`, `style`, `noscript`) to avoid regex creation overhead
@@ -290,7 +300,7 @@ class HTMLParser {
290
300
  } else {
291
301
  // No closing tag found; to avoid infinite loop, break similarly to previous behavior
292
302
  if (handler.continueOnParseError && handler.chars && html) {
293
- await handler.chars(html[0], prevTag, '');
303
+ await handler.chars(html[0], prevTag, '', prevAttrs, []);
294
304
  advance(1);
295
305
  } else {
296
306
  break;
@@ -302,10 +312,11 @@ class HTMLParser {
302
312
  if (handler.continueOnParseError) {
303
313
  // Skip the problematic character and continue
304
314
  if (handler.chars) {
305
- await handler.chars(fullHtml[pos], prevTag, '');
315
+ await handler.chars(fullHtml[pos], prevTag, '', prevAttrs, []);
306
316
  }
307
317
  advance(1);
308
318
  prevTag = '';
319
+ prevAttrs = [];
309
320
  continue;
310
321
  }
311
322
  const loc = getLineColumn(pos);
@@ -324,6 +335,23 @@ class HTMLParser {
324
335
  await parseEndTag();
325
336
  }
326
337
 
338
+ // Helper to extract minimal attribute info (name/value pairs) from raw attribute matches
339
+ // Used for whitespace collapsing logic—doesn’t need full processing
340
+ function extractAttrInfo(rawAttrs) {
341
+ if (!rawAttrs || !rawAttrs.length) return [];
342
+
343
+ const numCustomParts = handler.customAttrSurround ? handler.customAttrSurround.length * NCP : 0;
344
+ const baseIndex = 1 + numCustomParts;
345
+
346
+ return rawAttrs.map(args => {
347
+ // Extract attribute name (always at `baseIndex`)
348
+ const name = args[baseIndex];
349
+ // Extract value from double-quoted (`baseIndex + 2`), single-quoted (`baseIndex + 3`), or unquoted (`baseIndex + 4`)
350
+ const value = args[baseIndex + 2] ?? args[baseIndex + 3] ?? args[baseIndex + 4];
351
+ return { name: name?.toLowerCase(), value };
352
+ }).filter(attr => attr.name); // Filter out invalid entries
353
+ }
354
+
327
355
  function parseStartTag(input) {
328
356
  const start = input.match(startTagOpen);
329
357
  if (start) {
@@ -336,7 +364,7 @@ class HTMLParser {
336
364
  input = input.slice(consumed);
337
365
  let end, attr;
338
366
 
339
- // Safety limit: max length of input to check for attributes
367
+ // Safety limit: Max length of input to check for attributes
340
368
  // Protects against catastrophic backtracking on massive attribute values
341
369
  const MAX_ATTR_PARSE_LENGTH = 20000; // 20 KB should be enough for any reasonable tag
342
370
 
@@ -436,7 +464,7 @@ class HTMLParser {
436
464
  }
437
465
 
438
466
  async function parseEndTagAt(pos) {
439
- // Close all open elements up to pos (mirrors parseEndTags core branch)
467
+ // Close all open elements up to `pos` (mirrors `parseEndTag`’s core branch)
440
468
  for (let i = stack.length - 1; i >= pos; i--) {
441
469
  if (handler.end) {
442
470
  await handler.end(stack[i].tag, stack[i].attrs, true);
@@ -504,7 +532,7 @@ class HTMLParser {
504
532
  const attrs = match.attrs.map(function (args) {
505
533
  let name, value, customOpen, customClose, customAssign, quote;
506
534
 
507
- // Hackish workaround for FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
535
+ // Hackish workaround for Firefox bug, https://bugzilla.mozilla.org/show_bug.cgi?id=369778
508
536
  if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
509
537
  if (args[3] === '') { delete args[3]; }
510
538
  if (args[4] === '') { delete args[4]; }
@@ -561,6 +589,9 @@ class HTMLParser {
561
589
  unarySlash = '';
562
590
  }
563
591
 
592
+ // Store attributes for `prevAttrs` tracking (used in whitespace collapsing)
593
+ prevAttrs = attrs;
594
+
564
595
  if (handler.start) {
565
596
  await handler.start(tagName, attrs, unary, unarySlash);
566
597
  }
@@ -656,7 +687,7 @@ class Sorter {
656
687
 
657
688
  class TokenChain {
658
689
  constructor() {
659
- // Use Map instead of object properties for better performance
690
+ // Use map instead of object properties for better performance
660
691
  this.map = new Map();
661
692
  }
662
693
 
@@ -673,7 +704,7 @@ class TokenChain {
673
704
  const sorter = new Sorter();
674
705
  sorter.sorterMap = new Map();
675
706
 
676
- // Convert Map entries to array and sort by frequency (descending) then alphabetically
707
+ // Convert map entries to array and sort by frequency (descending), then alphabetically
677
708
  const entries = Array.from(this.map.entries()).sort((a, b) => {
678
709
  const m = a[1].arrays.length;
679
710
  const n = b[1].arrays.length;
@@ -722,11 +753,11 @@ class TokenChain {
722
753
  }
723
754
 
724
755
  /**
725
- * Preset configurations for HTML Minifier Next
756
+ * Preset configurations
726
757
  *
727
758
  * Presets provide curated option sets for common use cases:
728
- * - conservative: Safe minification suitable for most projects
729
- * - comprehensive: Aggressive minification for maximum file size reduction
759
+ * - `conservative`: Safe minification suitable for most projects
760
+ * - `comprehensive`: Aggressive minification for maximum file size reduction
730
761
  */
731
762
 
732
763
  const presets = {
@@ -747,7 +778,6 @@ const presets = {
747
778
  comprehensive: {
748
779
  caseSensitive: true,
749
780
  collapseBooleanAttributes: true,
750
- collapseInlineTagWhitespace: true,
751
781
  collapseWhitespace: true,
752
782
  continueOnParseError: true,
753
783
  decodeEntities: true,
@@ -772,7 +802,7 @@ const presets = {
772
802
 
773
803
  /**
774
804
  * Get preset configuration by name
775
- * @param {string} name - Preset name ('conservative' or 'comprehensive')
805
+ * @param {string} name - Preset name (conservative or comprehensive)
776
806
  * @returns {object|null} Preset options object or null if not found
777
807
  */
778
808
  function getPreset(name) {
@@ -871,7 +901,7 @@ async function replaceAsync(str, regex, asyncFn) {
871
901
  return str.replace(regex, () => data.shift());
872
902
  }
873
903
 
874
- // RegExp patterns (to avoid repeated allocations in hot paths)
904
+ // Regex patterns (to avoid repeated allocations in hot paths)
875
905
 
876
906
  const RE_WS_START = /^[ \n\r\t\f]+/;
877
907
  const RE_WS_END = /[ \n\r\t\f]+$/;
@@ -892,7 +922,7 @@ const RE_ATTR_WS_COLLAPSE = /[ \n\r\t\f]+/g;
892
922
  const RE_ATTR_WS_TRIM = /^[ \n\r\t\f]+|[ \n\r\t\f]+$/g;
893
923
  const RE_NUMERIC_VALUE = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
894
924
 
895
- // Inline element Sets for whitespace handling
925
+ // Inline element sets for whitespace handling
896
926
 
897
927
  // Non-empty elements that will maintain whitespace around them
898
928
  const inlineElementsToKeepWhitespaceAround = new Set(['a', 'abbr', 'acronym', 'b', 'bdi', 'bdo', 'big', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'mark', 'math', 'meter', 'nobr', 'object', 'output', 'progress', 'q', 'rb', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'svg', 'textarea', 'time', 'tt', 'u', 'var', 'wbr']);
@@ -903,6 +933,9 @@ const inlineElementsToKeepWhitespaceWithin = new Set(['a', 'abbr', 'acronym', 'b
903
933
  // Elements that will always maintain whitespace around them
904
934
  const inlineElementsToKeepWhitespace = new Set(['comment', 'img', 'input', 'wbr']);
905
935
 
936
+ // Form control elements (for conditional whitespace collapsing)
937
+ const formControlElements = new Set(['input', 'button', 'select', 'textarea', 'output', 'meter', 'progress']);
938
+
906
939
  // Default attribute values
907
940
 
908
941
  // Default attribute values (could apply to any element)
@@ -942,14 +975,17 @@ const tagDefaults = {
942
975
  // Script MIME types
943
976
 
944
977
  // https://mathiasbynens.be/demo/javascript-mime-type
945
- // https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type
978
+ // https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script
946
979
  const executableScriptsMimetypes = new Set([
947
980
  'text/javascript',
981
+ 'text/x-javascript',
948
982
  'text/ecmascript',
983
+ 'text/x-ecmascript',
949
984
  'text/jscript',
950
985
  'application/javascript',
951
986
  'application/x-javascript',
952
987
  'application/ecmascript',
988
+ 'application/x-ecmascript',
953
989
  'module'
954
990
  ]);
955
991
 
@@ -957,15 +993,15 @@ const keepScriptsMimetypes = new Set([
957
993
  'module'
958
994
  ]);
959
995
 
960
- // Boolean attribute Sets
996
+ // Boolean attribute sets
961
997
 
962
998
  const isSimpleBoolean = new Set(['allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'compact', 'controls', 'declare', 'default', 'defaultchecked', 'defaultmuted', 'defaultselected', 'defer', 'disabled', 'enabled', 'formnovalidate', 'hidden', 'indeterminate', 'inert', 'ismap', 'itemscope', 'loop', 'multiple', 'muted', 'nohref', 'noresize', 'noshade', 'novalidate', 'nowrap', 'open', 'pauseonexit', 'readonly', 'required', 'reversed', 'scoped', 'seamless', 'selected', 'sortable', 'truespeed', 'typemustmatch', 'visible']);
963
999
 
964
1000
  const isBooleanValue = new Set(['true', 'false']);
965
1001
 
966
- // `srcset` tags
1002
+ // `srcset` elements
967
1003
 
968
- const srcsetTags = new Set(['img', 'source']);
1004
+ const srcsetElements = new Set(['img', 'source']);
969
1005
 
970
1006
  // JSON script types
971
1007
 
@@ -981,7 +1017,7 @@ const jsonScriptTypes = new Set([
981
1017
  'speculationrules',
982
1018
  ]);
983
1019
 
984
- // Tag omission rules and element Sets
1020
+ // Tag omission rules and element sets
985
1021
 
986
1022
  // Tag omission rules from https://html.spec.whatwg.org/multipage/syntax.html#optional-tags with the following extensions:
987
1023
  // - retain `<body>` if followed by `<noscript>`
@@ -992,35 +1028,35 @@ const optionalStartTags = new Set(['html', 'head', 'body', 'colgroup', 'tbody'])
992
1028
 
993
1029
  const optionalEndTags = new Set(['html', 'head', 'body', 'li', 'dt', 'dd', 'p', 'rb', 'rt', 'rtc', 'rp', 'optgroup', 'option', 'colgroup', 'caption', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th']);
994
1030
 
995
- const headerTags = new Set(['meta', 'link', 'script', 'style', 'template', 'noscript']);
1031
+ const headerElements = new Set(['meta', 'link', 'script', 'style', 'template', 'noscript']);
996
1032
 
997
- const descriptionTags = new Set(['dt', 'dd']);
1033
+ const descriptionElements = new Set(['dt', 'dd']);
998
1034
 
999
- const pBlockTags = new Set(['address', 'article', 'aside', 'blockquote', 'details', 'dialog', 'div', 'dl', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'main', 'menu', 'nav', 'ol', 'p', 'pre', 'search', 'section', 'table', 'ul']);
1035
+ const pBlockElements = new Set(['address', 'article', 'aside', 'blockquote', 'details', 'dialog', 'div', 'dl', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'main', 'menu', 'nav', 'ol', 'p', 'pre', 'search', 'section', 'table', 'ul']);
1000
1036
 
1001
- const pInlineTags = new Set(['a', 'audio', 'del', 'ins', 'map', 'noscript', 'video']);
1037
+ const pInlineElements = new Set(['a', 'audio', 'del', 'ins', 'map', 'noscript', 'video']);
1002
1038
 
1003
1039
  const rubyEndTagOmission = new Set(['rb', 'rt', 'rtc', 'rp']); // `</rb>`, `</rt>`, `</rp>` can be omitted if followed by `<rb>`, `<rt>`, `<rtc>`, or `<rp>`
1004
1040
 
1005
1041
  const rubyRtcEndTagOmission = new Set(['rb', 'rtc']); // `</rtc>` can be omitted if followed by `<rb>` or `<rtc>` (not `<rt>` or `<rp>`)
1006
1042
 
1007
- const optionTag = new Set(['option', 'optgroup']);
1043
+ const optionElements = new Set(['option', 'optgroup']);
1008
1044
 
1009
- const tableContentTags = new Set(['tbody', 'tfoot']);
1045
+ const tableContentElements = new Set(['tbody', 'tfoot']);
1010
1046
 
1011
- const tableSectionTags = new Set(['thead', 'tbody', 'tfoot']);
1047
+ const tableSectionElements = new Set(['thead', 'tbody', 'tfoot']);
1012
1048
 
1013
- const cellTags = new Set(['td', 'th']);
1049
+ const cellElements = new Set(['td', 'th']);
1014
1050
 
1015
- const topLevelTags = new Set(['html', 'head', 'body']);
1051
+ const topLevelElements = new Set(['html', 'head', 'body']);
1016
1052
 
1017
- const compactTags = new Set(['html', 'body']);
1053
+ const compactElements = new Set(['html', 'body']);
1018
1054
 
1019
- const looseTags = new Set(['head', 'colgroup', 'caption']);
1055
+ const looseElements = new Set(['head', 'colgroup', 'caption']);
1020
1056
 
1021
- const trailingTags = new Set(['dt', 'thead']);
1057
+ const trailingElements = new Set(['dt', 'thead']);
1022
1058
 
1023
- const htmlTags = new Set(['a', 'abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', 'audio', 'b', 'base', 'basefont', 'bdi', 'bdo', 'bgsound', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'command', 'content', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'image', 'img', 'input', 'ins', 'isindex', 'kbd', 'keygen', 'label', 'legend', 'li', 'link', 'listing', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meta', 'meter', 'multicol', 'nav', 'nobr', 'noembed', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture', 'plaintext', 'pre', 'progress', 'q', 'rb', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'script', 'search', 'section', 'select', 'selectedcontent', 'shadow', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr', 'xmp']);
1059
+ const htmlElements = new Set(['a', 'abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', 'audio', 'b', 'base', 'basefont', 'bdi', 'bdo', 'bgsound', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'command', 'content', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'image', 'img', 'input', 'ins', 'isindex', 'kbd', 'keygen', 'label', 'legend', 'li', 'link', 'listing', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meta', 'meter', 'multicol', 'nav', 'nobr', 'noembed', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture', 'plaintext', 'pre', 'progress', 'q', 'rb', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'script', 'search', 'section', 'select', 'selectedcontent', 'shadow', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr', 'xmp']);
1024
1060
 
1025
1061
  // Empty attribute regex
1026
1062
 
@@ -1030,7 +1066,7 @@ const reEmptyAttribute = new RegExp(
1030
1066
 
1031
1067
  // Special content elements
1032
1068
 
1033
- const specialContentTags = new Set(['script', 'style']);
1069
+ const specialContentElements = new Set(['script', 'style']);
1034
1070
 
1035
1071
  // Imports
1036
1072
 
@@ -1094,7 +1130,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
1094
1130
  }
1095
1131
 
1096
1132
  if (trimLeft) {
1097
- // Non-breaking space is specifically handled inside the replacer function
1133
+ // No-break space is specifically handled inside the replacer function
1098
1134
  str = str.replace(/^[ \n\r\t\f\xA0]+/, function (spaces) {
1099
1135
  const conservative = !lineBreakBefore && options.conservativeCollapse;
1100
1136
  if (conservative && spaces === '\t') {
@@ -1105,7 +1141,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
1105
1141
  }
1106
1142
 
1107
1143
  if (trimRight) {
1108
- // Non-breaking space is specifically handled inside the replacer function
1144
+ // No-break space is specifically handled inside the replacer function
1109
1145
  str = str.replace(/[ \n\r\t\f\xA0]+$/, function (spaces) {
1110
1146
  const conservative = !lineBreakAfter && options.conservativeCollapse;
1111
1147
  if (conservative && spaces === '\t') {
@@ -1129,11 +1165,42 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
1129
1165
 
1130
1166
  // Collapse whitespace smartly based on surrounding tags
1131
1167
 
1132
- function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements, inlineTextSet) {
1168
+ function collapseWhitespaceSmart(str, prevTag, nextTag, prevAttrs, nextAttrs, options, inlineElements, inlineTextSet) {
1169
+ const prevTagName = prevTag && (prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag);
1170
+ const nextTagName = nextTag && (nextTag.charAt(0) === '/' ? nextTag.slice(1) : nextTag);
1171
+
1172
+ // Helper: Check if an input element has `type="hidden"`
1173
+ const isHiddenInput = (tagName, attrs) => {
1174
+ if (tagName !== 'input' || !attrs || !attrs.length) return false;
1175
+ const typeAttr = attrs.find(attr => attr.name === 'type');
1176
+ return typeAttr && typeAttr.value === 'hidden';
1177
+ };
1178
+
1179
+ // Check if prev/next are non-rendering (hidden) elements
1180
+ const prevIsHidden = isHiddenInput(prevTagName, prevAttrs);
1181
+ const nextIsHidden = isHiddenInput(nextTagName, nextAttrs);
1182
+
1133
1183
  let trimLeft = prevTag && !inlineElementsToKeepWhitespace.has(prevTag);
1184
+
1185
+ // Smart default behavior: Collapse space after non-rendering elements (`type="hidden"`)
1186
+ // This happens even in basic `collapseWhitespace` mode (safe optimization)
1187
+ if (!trimLeft && prevIsHidden && str && !/\S/.test(str)) {
1188
+ trimLeft = true;
1189
+ }
1190
+
1191
+ // Aggressive mode: Collapse between all form controls (pure whitespace only)
1192
+ const isPureWhitespace = str && !/\S/.test(str);
1193
+ if (!trimLeft && prevTagName && nextTagName &&
1194
+ options.collapseInlineTagWhitespace &&
1195
+ isPureWhitespace &&
1196
+ formControlElements.has(prevTagName) && formControlElements.has(nextTagName)) {
1197
+ trimLeft = true;
1198
+ }
1199
+
1134
1200
  if (trimLeft && !options.collapseInlineTagWhitespace) {
1135
1201
  trimLeft = prevTag.charAt(0) === '/' ? !inlineElements.has(prevTag.slice(1)) : !inlineTextSet.has(prevTag);
1136
1202
  }
1203
+
1137
1204
  // When `collapseInlineTagWhitespace` is enabled, still preserve whitespace around inline text elements
1138
1205
  if (trimLeft && options.collapseInlineTagWhitespace) {
1139
1206
  const tagName = prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag;
@@ -1141,10 +1208,26 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements,
1141
1208
  trimLeft = false;
1142
1209
  }
1143
1210
  }
1211
+
1144
1212
  let trimRight = nextTag && !inlineElementsToKeepWhitespace.has(nextTag);
1213
+
1214
+ // Smart default behavior: Collapse space before non-rendering elements (`type="hidden"`)
1215
+ if (!trimRight && nextIsHidden && str && !/\S/.test(str)) {
1216
+ trimRight = true;
1217
+ }
1218
+
1219
+ // Aggressive mode: Same as `trimLeft`
1220
+ if (!trimRight && prevTagName && nextTagName &&
1221
+ options.collapseInlineTagWhitespace &&
1222
+ isPureWhitespace &&
1223
+ formControlElements.has(prevTagName) && formControlElements.has(nextTagName)) {
1224
+ trimRight = true;
1225
+ }
1226
+
1145
1227
  if (trimRight && !options.collapseInlineTagWhitespace) {
1146
1228
  trimRight = nextTag.charAt(0) === '/' ? !inlineTextSet.has(nextTag.slice(1)) : !inlineElements.has(nextTag);
1147
1229
  }
1230
+
1148
1231
  // When `collapseInlineTagWhitespace` is enabled, still preserve whitespace around inline text elements
1149
1232
  if (trimRight && options.collapseInlineTagWhitespace) {
1150
1233
  const tagName = nextTag.charAt(0) === '/' ? nextTag.slice(1) : nextTag;
@@ -1152,6 +1235,7 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements,
1152
1235
  trimRight = false;
1153
1236
  }
1154
1237
  }
1238
+
1155
1239
  return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
1156
1240
  }
1157
1241
 
@@ -1172,7 +1256,6 @@ function canTrimWhitespace(tag) {
1172
1256
 
1173
1257
  // Wrap CSS declarations for inline styles and media queries
1174
1258
  // This ensures proper context for CSS minification
1175
-
1176
1259
  function wrapCSS(text, type) {
1177
1260
  switch (type) {
1178
1261
  case 'inline':
@@ -1254,7 +1337,6 @@ async function processScript(text, options, currentAttrs, minifyHTML) {
1254
1337
 
1255
1338
  /**
1256
1339
  * Lightweight SVG optimizations:
1257
- *
1258
1340
  * - Numeric precision reduction for coordinates and path data
1259
1341
  * - Whitespace removal in attribute values (numeric sequences)
1260
1342
  * - Default attribute removal (safe, well-documented defaults)
@@ -1348,7 +1430,7 @@ function minifyNumber(num, precision = 3) {
1348
1430
  if (num === '1.0' || num === '1.00' || num === '1.000') return '1';
1349
1431
 
1350
1432
  // Check cache
1351
- // (Note: uses input string as key, so “0.0000” and “0.00000” create separate entries.
1433
+ // (Note: Uses input string as key, so “0.0000” and “0.00000” create separate entries.
1352
1434
  // This is intentional to avoid parsing overhead.
1353
1435
  // Real-world SVG files from export tools typically use consistent formats.)
1354
1436
  const cacheKey = `${num}:${precision}`;
@@ -1387,15 +1469,15 @@ function minifyPathData(pathData, precision = 3) {
1387
1469
 
1388
1470
  // Remove unnecessary spaces around path commands
1389
1471
  // Safe to remove space after a command letter when it’s followed by a number (which may be negative)
1390
- // M 10 20 → M10 20, L -5 -3 → L-5-3
1472
+ // `M 10 20``M10 20`, `L -5 -3``L-5-3`
1391
1473
  result = result.replace(/([MLHVCSQTAZmlhvcsqtaz])\s+(?=-?\d)/g, '$1');
1392
1474
 
1393
1475
  // Safe to remove space before command letter when preceded by a number
1394
- // 0 L → 0L, 20 M → 20M
1476
+ // `0 L``0L`, `20 M``20M`
1395
1477
  result = result.replace(/(\d)\s+([MLHVCSQTAZmlhvcsqtaz])/g, '$1$2');
1396
1478
 
1397
1479
  // Safe to remove space before negative number when preceded by a number
1398
- // 10 -20 → 10-20 (numbers are separated by the minus sign)
1480
+ // `10 -20``10-20` (numbers are separated by the minus sign)
1399
1481
  result = result.replace(/(\d)\s+(-\d)/g, '$1$2');
1400
1482
 
1401
1483
  return result;
@@ -1404,9 +1486,9 @@ function minifyPathData(pathData, precision = 3) {
1404
1486
  /**
1405
1487
  * Minify whitespace in numeric attribute values
1406
1488
  * Examples:
1407
- * "10 , 20" → "10,20"
1408
- * "translate( 10 20 )" → "translate(10 20)"
1409
- * "100, 10 40, 198" → "100,10 40,198"
1489
+ * - “10 , 20" → "10,20"
1490
+ * - "translate( 10 20 )" → "translate(10 20)"
1491
+ * - "100, 10 40, 198" → "100,10 40,198"
1410
1492
  *
1411
1493
  * @param {string} value - Attribute value to minify
1412
1494
  * @returns {string} Minified value
@@ -1439,8 +1521,7 @@ function minifyColor(color) {
1439
1521
 
1440
1522
  // Don’t process values that aren’t simple colors (preserve case-sensitive references)
1441
1523
  // `url(#id)`, `var(--name)`, `inherit`, `currentColor`, etc.
1442
- if (trimmed.includes('url(') || trimmed.includes('var(') ||
1443
- trimmed === 'inherit' || trimmed === 'currentColor') {
1524
+ if (trimmed.includes('url(') || trimmed.includes('var(') || trimmed === 'inherit' || trimmed === 'currentColor') {
1444
1525
  return trimmed;
1445
1526
  }
1446
1527
 
@@ -1448,7 +1529,7 @@ function minifyColor(color) {
1448
1529
  const lower = trimmed.toLowerCase();
1449
1530
 
1450
1531
  // Shorten 6-digit hex to 3-digit when possible
1451
- // #aabbcc → #abc, #000000 → #000
1532
+ // `#aabbcc``#abc`, `#000000``#000`
1452
1533
  const hexMatch = lower.match(/^#([0-9a-f]{6})$/);
1453
1534
  if (hexMatch) {
1454
1535
  const hex = hexMatch[1];
@@ -1468,7 +1549,7 @@ function minifyColor(color) {
1468
1549
  return NAMED_COLORS[lower] || lower;
1469
1550
  }
1470
1551
 
1471
- // Convert rgb(255,255,255) to hex
1552
+ // Convert rgb() to hex
1472
1553
  const rgbMatch = lower.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
1473
1554
  if (rgbMatch) {
1474
1555
  const r = parseInt(rgbMatch[1], 10);
@@ -1499,14 +1580,14 @@ function minifyColor(color) {
1499
1580
  const NUMERIC_ATTRS = new Set([
1500
1581
  'd', // Path data
1501
1582
  'points', // Polygon/polyline points
1502
- 'viewBox', // viewBox coordinates
1583
+ 'viewBox', // `viewBox` coordinates
1503
1584
  'transform', // Transform functions
1504
1585
  'x', 'y', 'x1', 'y1', 'x2', 'y2', // Coordinates
1505
1586
  'cx', 'cy', 'r', 'rx', 'ry', // Circle/ellipse
1506
1587
  'width', 'height', // Dimensions
1507
1588
  'dx', 'dy', // Text offsets
1508
1589
  'offset', // Gradient offset
1509
- 'startOffset', // textPath
1590
+ 'startOffset', // `textPath`
1510
1591
  'pathLength', // Path length
1511
1592
  'stdDeviation', // Filter params
1512
1593
  'baseFrequency', // Turbulence
@@ -1690,8 +1771,8 @@ function shouldMinifyInnerHTML(options) {
1690
1771
  /**
1691
1772
  * @param {Partial<MinifierOptions>} inputOptions - User-provided options
1692
1773
  * @param {Object} deps - Dependencies from htmlminifier.js
1693
- * @param {Function} deps.getLightningCSS - Function to lazily load lightningcss
1694
- * @param {Function} deps.getTerser - Function to lazily load terser
1774
+ * @param {Function} deps.getLightningCSS - Function to lazily load Lightning CSS
1775
+ * @param {Function} deps.getTerser - Function to lazily load Terser
1695
1776
  * @param {Function} deps.getSwc - Function to lazily load @swc/core
1696
1777
  * @param {LRU} deps.cssMinifyCache - CSS minification cache
1697
1778
  * @param {LRU} deps.jsMinifyCache - JS minification cache
@@ -1728,7 +1809,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
1728
1809
  if (typeof value === 'string') {
1729
1810
  return new RegExp(value.replace(/^\/(.*)\/$/, '$1'));
1730
1811
  }
1731
- return value; // Already a RegExp or other type
1812
+ return value; // Already a RegExp or another type
1732
1813
  };
1733
1814
 
1734
1815
  const parseRegExpArray = (arr) => {
@@ -1748,9 +1829,25 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
1748
1829
  });
1749
1830
  };
1750
1831
 
1832
+ // Apply preset first if specified (so user options can override preset values)
1833
+ if (inputOptions.preset) {
1834
+ const preset = getPreset(inputOptions.preset);
1835
+ if (preset) {
1836
+ Object.assign(options, preset);
1837
+ } else {
1838
+ const available = getPresetNames().join(', ');
1839
+ console.warn(`HTML Minifier Next: Unknown preset “${inputOptions.preset}”. Available presets: ${available}`);
1840
+ }
1841
+ }
1842
+
1751
1843
  Object.keys(inputOptions).forEach(function (key) {
1752
1844
  const option = inputOptions[key];
1753
1845
 
1846
+ // Skip preset key—it’s already been processed
1847
+ if (key === 'preset') {
1848
+ return;
1849
+ }
1850
+
1754
1851
  if (key === 'caseSensitive') {
1755
1852
  if (option) {
1756
1853
  options.name = identity;
@@ -1865,7 +1962,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
1865
1962
  // Validate engine
1866
1963
  const supportedEngines = ['terser', 'swc'];
1867
1964
  if (!supportedEngines.includes(engine)) {
1868
- throw new Error(`Unsupported JS minifier engine: "${engine}". Supported engines: ${supportedEngines.join(', ')}`);
1965
+ throw new Error(`Unsupported JS minifier engine: “${engine}”. Supported engines: ${supportedEngines.join(', ')}`);
1869
1966
  }
1870
1967
 
1871
1968
  // Extract engine-specific options (excluding `engine` field itself)
@@ -1972,14 +2069,14 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
1972
2069
  relateUrlOptions = {};
1973
2070
  }
1974
2071
 
1975
- // Cache RelateURL instance for reuse (expensive to create)
2072
+ // Cache relateurl instance for reuse (expensive to create)
1976
2073
  const relateUrlInstance = new RelateURL(relateUrlOptions.site || '', relateUrlOptions);
1977
2074
 
1978
2075
  // Create instance-specific cache (results depend on site configuration)
1979
2076
  const instanceCache = urlMinifyCache ? new (urlMinifyCache.constructor)(500) : null;
1980
2077
 
1981
2078
  options.minifyURLs = function (text) {
1982
- // Fast-path: Skip if text doesn't look like a URL that needs processing
2079
+ // Fast-path: Skip if text doesnt look like a URL that needs processing
1983
2080
  // Only process if contains URL-like characters (`/`, `:`, `#`, `?`) or spaces that need encoding
1984
2081
  if (!/[/:?#\s]/.test(text)) {
1985
2082
  return text;
@@ -2011,17 +2108,17 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
2011
2108
  };
2012
2109
  } else if (key === 'minifySVG') {
2013
2110
  // Process SVG minification options
2014
- // Unlike minifyCSS/minifyJS, this is a simple options object, not a function
2111
+ // Unlike `minifyCSS`/`minifyJS`, this is a simple options object, not a function
2015
2112
  // The actual minification is applied inline during attribute processing
2016
2113
  options.minifySVG = getSVGMinifierOptions(option);
2017
2114
  } else if (key === 'customAttrCollapse') {
2018
- // Single RegExp pattern
2115
+ // Single regex pattern
2019
2116
  options[key] = parseRegExp(option);
2020
2117
  } else if (key === 'customAttrSurround') {
2021
2118
  // Nested array of RegExp pairs: `[[openRegExp, closeRegExp], …]`
2022
2119
  options[key] = parseNestedRegExpArray(option);
2023
2120
  } else if (['customAttrAssign', 'customEventAttributes', 'ignoreCustomComments', 'ignoreCustomFragments'].includes(key)) {
2024
- // Array of RegExp patterns
2121
+ // Array of regex patterns
2025
2122
  options[key] = parseRegExpArray(option);
2026
2123
  } else {
2027
2124
  options[key] = option;
@@ -2086,8 +2183,7 @@ function isAttributeRedundant(tag, attrName, attrValue, attrs) {
2086
2183
  const tagHasDefaults = tag in tagDefaults;
2087
2184
 
2088
2185
  // Check for legacy attribute rules (element- and attribute-specific)
2089
- const isLegacyAttr = (tag === 'script' && (attrName === 'language' || attrName === 'charset')) ||
2090
- (tag === 'a' && attrName === 'name');
2186
+ const isLegacyAttr = (tag === 'script' && (attrName === 'language' || attrName === 'charset')) || (tag === 'a' && attrName === 'name');
2091
2187
 
2092
2188
  // If none of these conditions apply, attribute cannot be redundant
2093
2189
  if (!hasGeneralDefault && !tagHasDefaults && !isLegacyAttr) {
@@ -2145,7 +2241,7 @@ function isStyleLinkTypeAttribute(attrValue = '') {
2145
2241
  return attrValue === '' || attrValue === 'text/css';
2146
2242
  }
2147
2243
 
2148
- function isStyleSheet(tag, attrs) {
2244
+ function isStyleElement(tag, attrs) {
2149
2245
  if (tag !== 'style') {
2150
2246
  return false;
2151
2247
  }
@@ -2202,11 +2298,11 @@ function isLinkType(tag, attrs, value) {
2202
2298
  }
2203
2299
 
2204
2300
  function isMediaQuery(tag, attrs, attrName) {
2205
- return attrName === 'media' && (isLinkType(tag, attrs, 'stylesheet') || isStyleSheet(tag, attrs));
2301
+ return attrName === 'media' && (isLinkType(tag, attrs, 'stylesheet') || isStyleElement(tag, attrs));
2206
2302
  }
2207
2303
 
2208
2304
  function isSrcset(attrName, tag) {
2209
- return attrName === 'srcset' && srcsetTags.has(tag);
2305
+ return attrName === 'srcset' && srcsetElements.has(tag);
2210
2306
  }
2211
2307
 
2212
2308
  function isMetaViewport(tag, attrs) {
@@ -2214,7 +2310,7 @@ function isMetaViewport(tag, attrs) {
2214
2310
  return false;
2215
2311
  }
2216
2312
  for (let i = 0, len = attrs.length; i < len; i++) {
2217
- if (attrs[i].name === 'name' && attrs[i].value === 'viewport') {
2313
+ if (attrs[i].name.toLowerCase() === 'name' && attrs[i].value.toLowerCase() === 'viewport') {
2218
2314
  return true;
2219
2315
  }
2220
2316
  }
@@ -2234,7 +2330,7 @@ function isContentSecurityPolicy(tag, attrs) {
2234
2330
  }
2235
2331
 
2236
2332
  function canDeleteEmptyAttribute(tag, attrName, attrValue, options) {
2237
- const isValueEmpty = !attrValue || /^\s*$/.test(attrValue);
2333
+ const isValueEmpty = !attrValue || attrValue.trim() === '';
2238
2334
  if (!isValueEmpty) {
2239
2335
  return false;
2240
2336
  }
@@ -2257,7 +2353,7 @@ function hasAttrName(name, attrs) {
2257
2353
 
2258
2354
  async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTMLSelf) {
2259
2355
  // Apply early whitespace normalization if enabled
2260
- // Preserves special spaces (non-breaking space, hair space, etc.) for consistency with `collapseWhitespace`
2356
+ // Preserves special spaces (no-break space, hair space, etc.) for consistency with `collapseWhitespace`
2261
2357
  if (options.collapseAttributeWhitespace) {
2262
2358
  // Fast path: Only process if whitespace exists (avoids regex overhead on clean values)
2263
2359
  if (RE_ATTR_WS_CHECK.test(attrValue)) {
@@ -2313,7 +2409,7 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
2313
2409
  try {
2314
2410
  attrValue = await options.minifyCSS(attrValue, 'inline');
2315
2411
  // After minification, check if CSS consists entirely of invalid properties (no values)
2316
- // E.g., `color:` or `margin:;padding:` should be treated as empty
2412
+ // I.e., `color:` or `margin:;padding:` should be treated as empty
2317
2413
  if (attrValue && /^(?:[a-z-]+:\s*;?\s*)+$/i.test(attrValue)) {
2318
2414
  attrValue = '';
2319
2415
  }
@@ -2433,13 +2529,13 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
2433
2529
  }
2434
2530
 
2435
2531
  if ((options.removeRedundantAttributes &&
2436
- isAttributeRedundant(tag, attrName, attrValue, attrs)) ||
2437
- (options.removeScriptTypeAttributes && tag === 'script' &&
2438
- attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue)) ||
2439
- (options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
2440
- attrName === 'type' && isStyleLinkTypeAttribute(attrValue)) ||
2441
- (options.insideSVG && options.minifySVG &&
2442
- shouldRemoveSVGAttribute(tag, attrName, attrValue, options.minifySVG))) {
2532
+ isAttributeRedundant(tag, attrName, attrValue, attrs)) ||
2533
+ (options.removeScriptTypeAttributes && tag === 'script' &&
2534
+ attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue)) ||
2535
+ (options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
2536
+ attrName === 'type' && isStyleLinkTypeAttribute(attrValue)) ||
2537
+ (options.insideSVG && options.minifySVG &&
2538
+ shouldRemoveSVGAttribute(tag, attrName, attrValue, options.minifySVG))) {
2443
2539
  return;
2444
2540
  }
2445
2541
 
@@ -2448,7 +2544,7 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
2448
2544
  }
2449
2545
 
2450
2546
  if (options.removeEmptyAttributes &&
2451
- canDeleteEmptyAttribute(tag, attrName, attrValue, options)) {
2547
+ canDeleteEmptyAttribute(tag, attrName, attrValue, options)) {
2452
2548
  return;
2453
2549
  }
2454
2550
 
@@ -2471,19 +2567,35 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
2471
2567
  let attrFragment;
2472
2568
  let emittedAttrValue;
2473
2569
 
2474
- if (typeof attrValue !== 'undefined' && (!options.removeAttributeQuotes ||
2475
- attrValue.indexOf(uidAttr) !== -1 || !canRemoveAttributeQuotes(attrValue))) {
2570
+ // Determine if we need to add/keep quotes
2571
+ const shouldAddQuotes = typeof attrValue !== 'undefined' && (
2572
+ // If `removeAttributeQuotes` is enabled, add quotes only if they can’t be removed
2573
+ (options.removeAttributeQuotes && (attrValue.indexOf(uidAttr) !== -1 || !canRemoveAttributeQuotes(attrValue))) ||
2574
+ // If `removeAttributeQuotes` is not enabled, preserve original quote style or add quotes if value requires them
2575
+ (!options.removeAttributeQuotes && (attrQuote !== '' || !canRemoveAttributeQuotes(attrValue) ||
2576
+ // Special case: With `removeTagWhitespace`, unquoted values that aren’t last will have space added,
2577
+ // which can create ambiguous/invalid HTML—add quotes to be safe
2578
+ (options.removeTagWhitespace && attrQuote === '' && !isLast)))
2579
+ );
2580
+
2581
+ if (shouldAddQuotes) {
2476
2582
  // Determine the appropriate quote character
2477
2583
  if (!options.preventAttributesEscaping) {
2478
- // Normal mode: choose quotes and escape
2479
- attrQuote = chooseAttributeQuote(attrValue, options);
2584
+ // Normal mode: Choose optimal quote type to minimize escaping
2585
+ // unless we’re preserving original quotes and they don’t need escaping
2586
+ const needsEscaping = (attrQuote === '"' && attrValue.indexOf('"') !== -1) || (attrQuote === "'" && attrValue.indexOf("'") !== -1);
2587
+
2588
+ if (options.removeAttributeQuotes || typeof options.quoteCharacter !== 'undefined' || needsEscaping || attrQuote === '') {
2589
+ attrQuote = chooseAttributeQuote(attrValue, options);
2590
+ }
2591
+
2480
2592
  if (attrQuote === '"') {
2481
2593
  attrValue = attrValue.replace(/"/g, '&#34;');
2482
2594
  } else {
2483
2595
  attrValue = attrValue.replace(/'/g, '&#39;');
2484
2596
  }
2485
2597
  } else {
2486
- // `preventAttributesEscaping` mode: choose safe quotes but don't escape
2598
+ // `preventAttributesEscaping` mode: Choose safe quotes but don't escape
2487
2599
  // except when both quote types are present—then escape to prevent invalid HTML
2488
2600
  const hasDoubleQuote = attrValue.indexOf('"') !== -1;
2489
2601
  const hasSingleQuote = attrValue.indexOf("'") !== -1;
@@ -2502,8 +2614,18 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
2502
2614
  attrQuote = "'";
2503
2615
  } else if (attrQuote === "'" && hasSingleQuote && !hasDoubleQuote) {
2504
2616
  attrQuote = '"';
2505
- // Fallback for invalid/unsupported attrQuote values (not `"`, `'`, or empty string): Choose safe default based on value content
2506
- } else if (attrQuote !== '"' && attrQuote !== "'" && attrQuote !== '') {
2617
+ // If no quote character yet (empty string), choose based on content
2618
+ } else if (attrQuote === '') {
2619
+ if (hasSingleQuote && !hasDoubleQuote) {
2620
+ attrQuote = '"';
2621
+ } else if (hasDoubleQuote && !hasSingleQuote) {
2622
+ attrQuote = "'";
2623
+ } else {
2624
+ attrQuote = '"';
2625
+ }
2626
+ // Fallback for invalid/unsupported attrQuote values (not `"`, `'`, or empty string):
2627
+ // Choose safe default based on value content
2628
+ } else if (attrQuote !== '"' && attrQuote !== "'") {
2507
2629
  if (hasSingleQuote && !hasDoubleQuote) {
2508
2630
  attrQuote = '"';
2509
2631
  } else if (hasDoubleQuote && !hasSingleQuote) {
@@ -2513,7 +2635,22 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
2513
2635
  }
2514
2636
  }
2515
2637
  } else {
2516
- attrQuote = options.quoteCharacter === '\'' ? '\'' : '"';
2638
+ // `quoteCharacter` is explicitly set
2639
+ const preferredQuote = options.quoteCharacter === '\'' ? '\'' : '"';
2640
+ // Safety check: If the preferred quote conflicts with value content, switch to the opposite quote
2641
+ if ((preferredQuote === '"' && hasDoubleQuote && !hasSingleQuote) || (preferredQuote === "'" && hasSingleQuote && !hasDoubleQuote)) {
2642
+ attrQuote = preferredQuote === '"' ? "'" : '"';
2643
+ } else if ((preferredQuote === '"' && hasDoubleQuote && hasSingleQuote) || (preferredQuote === "'" && hasSingleQuote && hasDoubleQuote)) {
2644
+ // Both quote types present: Fall back to escaping despite `preventAttributesEscaping`
2645
+ attrQuote = preferredQuote;
2646
+ if (attrQuote === '"') {
2647
+ attrValue = attrValue.replace(/"/g, '&#34;');
2648
+ } else {
2649
+ attrValue = attrValue.replace(/'/g, '&#39;');
2650
+ }
2651
+ } else {
2652
+ attrQuote = preferredQuote;
2653
+ }
2517
2654
  }
2518
2655
  }
2519
2656
  emittedAttrValue = attrQuote + attrValue + attrQuote;
@@ -2521,15 +2658,17 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
2521
2658
  emittedAttrValue += ' ';
2522
2659
  }
2523
2660
  } else if (isLast && !hasUnarySlash) {
2524
- // Last attribute in a non-self-closing tag: no space needed
2661
+ // Last attribute in a non-self-closing tag:
2662
+ // No space needed
2525
2663
  emittedAttrValue = attrValue;
2526
2664
  } else {
2527
- // Not last attribute, or is a self-closing tag: add space
2665
+ // Not last attribute, or is a self-closing tag:
2666
+ // Unquoted values must have space after them to delimit from next attribute
2528
2667
  emittedAttrValue = attrValue + ' ';
2529
2668
  }
2530
2669
 
2531
2670
  if (typeof attrValue === 'undefined' || (options.collapseBooleanAttributes &&
2532
- isBooleanAttribute(attrName.toLowerCase(), (attrValue || '').toLowerCase()))) {
2671
+ isBooleanAttribute(attrName.toLowerCase(), (attrValue || '').toLowerCase()))) {
2533
2672
  attrFragment = attrName;
2534
2673
  if (!isLast) {
2535
2674
  attrFragment += ' ';
@@ -2552,7 +2691,7 @@ function canRemoveParentTag(optionalStartTag, tag) {
2552
2691
  case 'head':
2553
2692
  return true;
2554
2693
  case 'body':
2555
- return !headerTags.has(tag);
2694
+ return !headerElements.has(tag);
2556
2695
  case 'colgroup':
2557
2696
  return tag === 'col';
2558
2697
  case 'tbody':
@@ -2566,7 +2705,7 @@ function isStartTagMandatory(optionalEndTag, tag) {
2566
2705
  case 'colgroup':
2567
2706
  return optionalEndTag === 'colgroup';
2568
2707
  case 'tbody':
2569
- return tableSectionTags.has(optionalEndTag);
2708
+ return tableSectionElements.has(optionalEndTag);
2570
2709
  }
2571
2710
  return false;
2572
2711
  }
@@ -2585,9 +2724,9 @@ function canRemovePrecedingTag(optionalEndTag, tag) {
2585
2724
  return tag === optionalEndTag;
2586
2725
  case 'dt':
2587
2726
  case 'dd':
2588
- return descriptionTags.has(tag);
2727
+ return descriptionElements.has(tag);
2589
2728
  case 'p':
2590
- return pBlockTags.has(tag);
2729
+ return pBlockElements.has(tag);
2591
2730
  case 'rb':
2592
2731
  case 'rt':
2593
2732
  case 'rp':
@@ -2595,15 +2734,15 @@ function canRemovePrecedingTag(optionalEndTag, tag) {
2595
2734
  case 'rtc':
2596
2735
  return rubyRtcEndTagOmission.has(tag);
2597
2736
  case 'option':
2598
- return optionTag.has(tag);
2737
+ return optionElements.has(tag);
2599
2738
  case 'thead':
2600
2739
  case 'tbody':
2601
- return tableContentTags.has(tag);
2740
+ return tableContentElements.has(tag);
2602
2741
  case 'tfoot':
2603
2742
  return tag === 'tbody';
2604
2743
  case 'td':
2605
2744
  case 'th':
2606
- return cellTags.has(tag);
2745
+ return cellElements.has(tag);
2607
2746
  }
2608
2747
  return false;
2609
2748
  }
@@ -2706,7 +2845,7 @@ function parseRemoveEmptyElementsExcept(input, options) {
2706
2845
  if (typeof item === 'string') {
2707
2846
  const spec = parseElementSpec(item, options);
2708
2847
  if (!spec && options.log) {
2709
- options.log('Warning: Unable to parse “removeEmptyElementsExcept” specification: "' + item + '"');
2848
+ options.log('Warning: Unable to parse “removeEmptyElementsExcept” specification: ' + item + '');
2710
2849
  }
2711
2850
  return spec;
2712
2851
  }
@@ -3219,7 +3358,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
3219
3358
  }
3220
3359
 
3221
3360
  // Pre-compile regex patterns for reuse (performance optimization)
3222
- // These must be declared before scan() since scan uses them
3361
+ // These must be declared before `scan()` since scan uses them
3223
3362
  const whitespaceSplitPatternScan = /[ \t\n\f\r]+/;
3224
3363
  const whitespaceSplitPatternSort = /[ \n\f\r]+/;
3225
3364
 
@@ -3251,9 +3390,9 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
3251
3390
  chars: async function (text) {
3252
3391
  // Only recursively scan HTML content, not JSON-LD or other non-HTML script types
3253
3392
  // `scan()` is for analyzing HTML attribute order, not for parsing JSON
3254
- if (options.processScripts && specialContentTags.has(currentTag) &&
3255
- options.processScripts.indexOf(currentType) > -1 &&
3256
- currentType === 'text/html') {
3393
+ if (options.processScripts && specialContentElements.has(currentTag) &&
3394
+ options.processScripts.indexOf(currentType) > -1 &&
3395
+ currentType === 'text/html') {
3257
3396
  await scan(text);
3258
3397
  }
3259
3398
  },
@@ -3276,7 +3415,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
3276
3415
  // For the first pass, create a copy of options and disable aggressive minification.
3277
3416
  // Keep attribute transformations (like `removeStyleLinkTypeAttributes`) for accurate analysis.
3278
3417
  // This is safe because `createSortFns` is called before custom fragment UID markers (`uidAttr`) are added.
3279
- // Note: `htmlmin:ignore` UID markers (uidIgnore) already exist and are expanded for analysis.
3418
+ // Note: `htmlmin:ignore` UID markers (`uidIgnore`) already exist and are expanded for analysis.
3280
3419
  const firstPassOptions = Object.assign({}, options, {
3281
3420
  // Disable sorting for the analysis pass
3282
3421
  sortAttributes: false,
@@ -3295,7 +3434,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
3295
3434
  });
3296
3435
 
3297
3436
  // Temporarily enable `continueOnParseError` for the `scan()` function call below.
3298
- // Note: `firstPassOptions` already has `continueOnParseError: true` for the minifyHTML call.
3437
+ // Note: `firstPassOptions` already has `continueOnParseError: true` for the `minifyHTML` call.
3299
3438
  const originalContinueOnParseError = options.continueOnParseError;
3300
3439
  options.continueOnParseError = true;
3301
3440
 
@@ -3308,7 +3447,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
3308
3447
  : null;
3309
3448
 
3310
3449
  try {
3311
- // Expand UID tokens back to original content for frequency analysis
3450
+ // Expand UID tokens back to the original content for frequency analysis
3312
3451
  let expandedValue = value;
3313
3452
  if (uidReplacePattern) {
3314
3453
  expandedValue = value.replace(uidReplacePattern, function (match, index) {
@@ -3357,7 +3496,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
3357
3496
  attrOrderCache.set(cacheKey, sortedNames);
3358
3497
  }
3359
3498
 
3360
- // Apply the sorted order to attrs
3499
+ // Apply the sorted order to `attrs`
3361
3500
  const attrMap = Object.create(null);
3362
3501
  names.forEach(function (name, index) {
3363
3502
  (attrMap[name] || (attrMap[name] = [])).push(attrs[index]);
@@ -3444,7 +3583,7 @@ async function minifyHTML(value, options, partialMarkup) {
3444
3583
  const customElementsInput = options.inlineCustomElements ?? [];
3445
3584
  const customElementsArr = Array.isArray(customElementsInput) ? customElementsInput : Array.from(customElementsInput);
3446
3585
  const normalizedCustomElements = customElementsArr.map(name => options.name(name));
3447
- // Fast path: Reuse base Sets if no custom elements
3586
+ // Fast path: Reuse base sets if no custom elements
3448
3587
  const inlineTextSet = normalizedCustomElements.length
3449
3588
  ? new Set([...inlineElementsToKeepWhitespaceWithin, ...normalizedCustomElements])
3450
3589
  : inlineElementsToKeepWhitespaceWithin;
@@ -3464,7 +3603,7 @@ async function minifyHTML(value, options, partialMarkup) {
3464
3603
  }
3465
3604
 
3466
3605
  // Temporarily replace ignored chunks with comments, so that we don’t have to worry what’s there.
3467
- // For all we care there might be completely-horribly-broken-alien-non-html-emoj-cthulhu-filled content
3606
+ // For all we care there might be completely-horribly-broken-alien-non-html-emoji-cthulhu-filled content
3468
3607
  value = value.replace(/<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g, function (match, group1) {
3469
3608
  if (!uidIgnore) {
3470
3609
  uidIgnore = uniqueId(value);
@@ -3485,7 +3624,7 @@ async function minifyHTML(value, options, partialMarkup) {
3485
3624
  // Create sort functions after `htmlmin:ignore` processing but before custom fragment UID markers
3486
3625
  // This allows proper frequency analysis with access to ignored content via UID tokens
3487
3626
  if ((options.sortAttributes && typeof options.sortAttributes !== 'function') ||
3488
- (options.sortClassName && typeof options.sortClassName !== 'function')) {
3627
+ (options.sortClassName && typeof options.sortClassName !== 'function')) {
3489
3628
  await createSortFns(value, options, uidIgnore, null, ignoredMarkupChunks);
3490
3629
  }
3491
3630
 
@@ -3547,11 +3686,11 @@ async function minifyHTML(value, options, partialMarkup) {
3547
3686
  });
3548
3687
  }
3549
3688
 
3550
- function _canCollapseWhitespace(tag, attrs) {
3689
+ function canCollapseWhitespace$1(tag, attrs) {
3551
3690
  return options.canCollapseWhitespace(tag, attrs, canCollapseWhitespace);
3552
3691
  }
3553
3692
 
3554
- function _canTrimWhitespace(tag, attrs) {
3693
+ function canTrimWhitespace$1(tag, attrs) {
3555
3694
  return options.canTrimWhitespace(tag, attrs, canTrimWhitespace);
3556
3695
  }
3557
3696
 
@@ -3573,12 +3712,12 @@ async function minifyHTML(value, options, partialMarkup) {
3573
3712
 
3574
3713
  // Look for trailing whitespaces, bypass any inline tags
3575
3714
  function trimTrailingWhitespace(index, nextTag) {
3576
- for (let endTag = null; index >= 0 && _canTrimWhitespace(endTag); index--) {
3715
+ for (let endTag = null; index >= 0 && canTrimWhitespace$1(endTag); index--) {
3577
3716
  const str = buffer[index];
3578
3717
  const match = str.match(/^<\/([\w:-]+)>$/);
3579
3718
  if (match) {
3580
3719
  endTag = match[1];
3581
- } else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, options, inlineElements, inlineTextSet))) {
3720
+ } else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, [], [], options, inlineElements, inlineTextSet))) {
3582
3721
  break;
3583
3722
  }
3584
3723
  }
@@ -3627,10 +3766,10 @@ async function minifyHTML(value, options, partialMarkup) {
3627
3766
 
3628
3767
  let optional = options.removeOptionalTags;
3629
3768
  if (optional) {
3630
- const htmlTag = htmlTags.has(tag);
3769
+ const htmlTag = htmlElements.has(tag);
3631
3770
  // `<html>` may be omitted if first thing inside is not a comment
3632
3771
  // `<head>` may be omitted if first thing inside is an element
3633
- // `<body>` may be omitted if first thing inside is not space, comment, `<meta>`, `<link>`, `<script>`, <`style>`, or `<template>`
3772
+ // `<body>` may be omitted if first thing inside is not space, comment, `<meta>`, `<link>`, `<script>`, `<style>`, or `<template>`
3634
3773
  // `<colgroup>` may be omitted if first thing inside is `<col>`
3635
3774
  // `<tbody>` may be omitted if first thing inside is `<tr>`
3636
3775
  if (htmlTag && canRemoveParentTag(optionalStartTag, tag)) {
@@ -3647,16 +3786,16 @@ async function minifyHTML(value, options, partialMarkup) {
3647
3786
  optionalEndTag = '';
3648
3787
  }
3649
3788
 
3650
- // Set whitespace flags for nested tags (e.g., <code> within a <pre>)
3789
+ // Set whitespace flags for nested tags (e.g., `<code>` within a `<pre>`)
3651
3790
  if (options.collapseWhitespace) {
3652
3791
  if (!stackNoTrimWhitespace.length) {
3653
3792
  squashTrailingWhitespace(tag);
3654
3793
  }
3655
3794
  if (!unary) {
3656
- if (!_canTrimWhitespace(tag, attrs) || stackNoTrimWhitespace.length) {
3795
+ if (!canTrimWhitespace$1(tag, attrs) || stackNoTrimWhitespace.length) {
3657
3796
  stackNoTrimWhitespace.push(tag);
3658
3797
  }
3659
- if (!_canCollapseWhitespace(tag, attrs) || stackNoCollapseWhitespace.length) {
3798
+ if (!canCollapseWhitespace$1(tag, attrs) || stackNoCollapseWhitespace.length) {
3660
3799
  stackNoCollapseWhitespace.push(tag);
3661
3800
  }
3662
3801
  }
@@ -3712,7 +3851,7 @@ async function minifyHTML(value, options, partialMarkup) {
3712
3851
  squashTrailingWhitespace('/' + tag);
3713
3852
  }
3714
3853
  if (stackNoCollapseWhitespace.length &&
3715
- tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
3854
+ tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
3716
3855
  stackNoCollapseWhitespace.pop();
3717
3856
  }
3718
3857
  }
@@ -3725,7 +3864,7 @@ async function minifyHTML(value, options, partialMarkup) {
3725
3864
 
3726
3865
  if (options.removeOptionalTags) {
3727
3866
  // `<html>`, `<head>` or `<body>` may be omitted if the element is empty
3728
- if (isElementEmpty && topLevelTags.has(optionalStartTag)) {
3867
+ if (isElementEmpty && topLevelElements.has(optionalStartTag)) {
3729
3868
  removeStartTag();
3730
3869
  }
3731
3870
  optionalStartTag = '';
@@ -3733,7 +3872,7 @@ async function minifyHTML(value, options, partialMarkup) {
3733
3872
  // `</head>` may be omitted if not followed by space or comment
3734
3873
  // `</p>` may be omitted if no more content in non-`</a>` parent
3735
3874
  // except for `</dt>` or `</thead>`, end tags may be omitted if no more content in parent element
3736
- if (tag && optionalEndTag && !trailingTags.has(optionalEndTag) && (optionalEndTag !== 'p' || !pInlineTags.has(tag))) {
3875
+ if (tag && optionalEndTag && !trailingElements.has(optionalEndTag) && (optionalEndTag !== 'p' || !pInlineElements.has(tag))) {
3737
3876
  removeEndTag();
3738
3877
  }
3739
3878
  optionalEndTag = optionalEndTags.has(tag) ? tag : '';
@@ -3780,10 +3919,12 @@ async function minifyHTML(value, options, partialMarkup) {
3780
3919
  }
3781
3920
  }
3782
3921
  },
3783
- chars: async function (text, prevTag, nextTag) {
3922
+ chars: async function (text, prevTag, nextTag, prevAttrs, nextAttrs) {
3784
3923
  prevTag = prevTag === '' ? 'comment' : prevTag;
3785
3924
  nextTag = nextTag === '' ? 'comment' : nextTag;
3786
- if (options.decodeEntities && text && !specialContentTags.has(currentTag)) {
3925
+ prevAttrs = prevAttrs || [];
3926
+ nextAttrs = nextAttrs || [];
3927
+ if (options.decodeEntities && text && !specialContentElements.has(currentTag)) {
3787
3928
  if (text.indexOf('&') !== -1) {
3788
3929
  text = entities.decodeHTML(text);
3789
3930
  }
@@ -3819,7 +3960,7 @@ async function minifyHTML(value, options, partialMarkup) {
3819
3960
  }
3820
3961
  }
3821
3962
  if (prevTag || nextTag) {
3822
- text = collapseWhitespaceSmart(text, prevTag, nextTag, options, inlineElements, inlineTextSet);
3963
+ text = collapseWhitespaceSmart(text, prevTag, nextTag, prevAttrs, nextAttrs, options, inlineElements, inlineTextSet);
3823
3964
  } else {
3824
3965
  text = collapseWhitespace(text, options, true, true);
3825
3966
  }
@@ -3831,13 +3972,13 @@ async function minifyHTML(value, options, partialMarkup) {
3831
3972
  text = collapseWhitespace(text, options, false, false, true);
3832
3973
  }
3833
3974
  }
3834
- if (specialContentTags.has(currentTag) && (options.processScripts || hasJsonScriptType(currentAttrs))) {
3975
+ if (specialContentElements.has(currentTag) && (options.processScripts || hasJsonScriptType(currentAttrs))) {
3835
3976
  text = await processScript(text, options, currentAttrs, minifyHTML);
3836
3977
  }
3837
3978
  if (isExecutableScript(currentTag, currentAttrs)) {
3838
3979
  text = await options.minifyJS(text);
3839
3980
  }
3840
- if (isStyleSheet(currentTag, currentAttrs)) {
3981
+ if (isStyleElement(currentTag, currentAttrs)) {
3841
3982
  text = await options.minifyCSS(text);
3842
3983
  }
3843
3984
  if (options.removeOptionalTags && text) {
@@ -3849,7 +3990,7 @@ async function minifyHTML(value, options, partialMarkup) {
3849
3990
  optionalStartTag = '';
3850
3991
  // `</html>` or `</body>` may be omitted if not followed by comment
3851
3992
  // `</head>`, `</colgroup>`, or `</caption>` may be omitted if not followed by space or comment
3852
- if (compactTags.has(optionalEndTag) || (looseTags.has(optionalEndTag) && !/^\s/.test(text))) {
3993
+ if (compactElements.has(optionalEndTag) || (looseElements.has(optionalEndTag) && !/^\s/.test(text))) {
3853
3994
  removeEndTag();
3854
3995
  }
3855
3996
  // Don’t reset optionalEndTag if text is only whitespace and will be collapsed (not conservatively)
@@ -3858,11 +3999,11 @@ async function minifyHTML(value, options, partialMarkup) {
3858
3999
  }
3859
4000
  }
3860
4001
  charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
3861
- if (options.decodeEntities && text && !specialContentTags.has(currentTag)) {
4002
+ if (options.decodeEntities && text && !specialContentElements.has(currentTag)) {
3862
4003
  // Escape any `&` symbols that start either:
3863
- // 1) a legacy named character reference (i.e., one that doesn’t end with `;`)
4004
+ // 1) a legacy-named character reference (i.e., one that doesn’t end with `;`)
3864
4005
  // 2) or any other character reference (i.e., one that does end with `;`)
3865
- // Note that `&` can be escaped as `&amp`, without the semi-colon.
4006
+ // Note that `&` can be escaped as `&amp`, without the semicolon.
3866
4007
  // https://mathiasbynens.be/notes/ambiguous-ampersands
3867
4008
  if (text.indexOf('&') !== -1) {
3868
4009
  text = text.replace(RE_LEGACY_ENTITIES, '&amp$1');
@@ -3927,7 +4068,7 @@ async function minifyHTML(value, options, partialMarkup) {
3927
4068
 
3928
4069
  // Only collapse whitespace if both blocks contain HTML (start with `<`)
3929
4070
  // Don’t collapse if either contains plain text, as that would change meaning
3930
- // Note: This check will match HTML comments (`<!-- … -->`), but the tag-name
4071
+ // Note: This check will match HTML comments (`<!-- … -->`), but the tag name
3931
4072
  // regex below requires starting with a letter, so comments are intentionally
3932
4073
  // excluded by the `currentTagMatch && prevTagMatch` guard
3933
4074
  if (currentContent && prevContent && /^\s*</.test(currentContent) && /^\s*</.test(prevContent)) {
@@ -3988,11 +4129,11 @@ async function minifyHTML(value, options, partialMarkup) {
3988
4129
  if (options.removeOptionalTags) {
3989
4130
  // `<html>` may be omitted if first thing inside is not a comment
3990
4131
  // `<head>` or `<body>` may be omitted if empty
3991
- if (topLevelTags.has(optionalStartTag)) {
4132
+ if (topLevelElements.has(optionalStartTag)) {
3992
4133
  removeStartTag();
3993
4134
  }
3994
4135
  // except for `</dt>` or `</thead>`, end tags may be omitted if no more content in parent element
3995
- if (optionalEndTag && !trailingTags.has(optionalEndTag)) {
4136
+ if (optionalEndTag && !trailingElements.has(optionalEndTag)) {
3996
4137
  removeEndTag();
3997
4138
  }
3998
4139
  }