html-minifier-next 4.16.4 → 4.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2617,7 +2617,7 @@ function decodeHTMLStrict(htmlString) {
2617
2617
  return htmlDecoder(htmlString, DecodingMode.Strict);
2618
2618
  }
2619
2619
 
2620
- /*!
2620
+ /*
2621
2621
  * HTML Parser By John Resig (ejohn.org)
2622
2622
  * Modified by Juriy “kangax” Zaytsev
2623
2623
  * Original code by Erik Arvidsson, Mozilla Public License
@@ -2628,10 +2628,10 @@ function decodeHTMLStrict(htmlString) {
2628
2628
  * Use like so:
2629
2629
  *
2630
2630
  * HTMLParser(htmlString, {
2631
- * start: function(tag, attrs, unary) {},
2632
- * end: function(tag) {},
2633
- * chars: function(text) {},
2634
- * comment: function(text) {}
2631
+ * start: function(tag, attrs, unary) {},
2632
+ * end: function(tag) {},
2633
+ * chars: function(text) {},
2634
+ * comment: function(text) {}
2635
2635
  * });
2636
2636
  */
2637
2637
 
@@ -2654,7 +2654,7 @@ const singleAttrValues = [
2654
2654
  ];
2655
2655
  // https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
2656
2656
  const qnameCapture = (function () {
2657
- // based on https://www.npmjs.com/package/ncname
2657
+ // https://www.npmjs.com/package/ncname
2658
2658
  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';
2659
2659
  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';
2660
2660
  const extender = '\\xB7\\u02D0\\u02D1\\u0387\\u0640\\u0E46\\u0EC6\\u3005\\u3031-\\u3035\\u309D\\u309E\\u30FC-\\u30FE';
@@ -2694,7 +2694,7 @@ const nonPhrasing = new CaseInsensitiveSet(['address', 'article', 'aside', 'base
2694
2694
  const reCache = {};
2695
2695
 
2696
2696
  // Pre-compiled regexes for common special elements (`script`, `style`, `noscript`)
2697
- // These are used frequently and pre-compiling them avoids regex creation overhead
2697
+ // These are used frequently, and pre-compiling them avoids regex creation overhead
2698
2698
  const preCompiledStackedTags = {
2699
2699
  'script': /([\s\S]*?)<\/script[^>]*>/i,
2700
2700
  'style': /([\s\S]*?)<\/style[^>]*>/i,
@@ -2757,6 +2757,7 @@ class HTMLParser {
2757
2757
  // Use cached attribute regex for this handler configuration
2758
2758
  const attribute = getAttrRegexForHandler(handler);
2759
2759
  let prevTag = undefined, nextTag = undefined;
2760
+ let prevAttrs = [], nextAttrs = [];
2760
2761
 
2761
2762
  // Index-based parsing
2762
2763
  let pos = 0;
@@ -2800,6 +2801,7 @@ class HTMLParser {
2800
2801
  }
2801
2802
  advance(commentEnd + 3);
2802
2803
  prevTag = '';
2804
+ prevAttrs = [];
2803
2805
  continue;
2804
2806
  }
2805
2807
  }
@@ -2814,6 +2816,7 @@ class HTMLParser {
2814
2816
  }
2815
2817
  advance(conditionalEnd + 2);
2816
2818
  prevTag = '';
2819
+ prevAttrs = [];
2817
2820
  continue;
2818
2821
  }
2819
2822
  }
@@ -2826,6 +2829,7 @@ class HTMLParser {
2826
2829
  }
2827
2830
  advance(doctypeMatch[0].length);
2828
2831
  prevTag = '';
2832
+ prevAttrs = [];
2829
2833
  continue;
2830
2834
  }
2831
2835
 
@@ -2835,6 +2839,7 @@ class HTMLParser {
2835
2839
  advance(endTagMatch[0].length);
2836
2840
  await parseEndTag(endTagMatch[0], endTagMatch[1]);
2837
2841
  prevTag = '/' + endTagMatch[1].toLowerCase();
2842
+ prevAttrs = [];
2838
2843
  continue;
2839
2844
  }
2840
2845
 
@@ -2867,19 +2872,24 @@ class HTMLParser {
2867
2872
  let nextTagMatch = parseStartTag(nextHtml);
2868
2873
  if (nextTagMatch) {
2869
2874
  nextTag = nextTagMatch.tagName;
2875
+ // Extract minimal attribute info for whitespace logic (just name/value pairs)
2876
+ nextAttrs = extractAttrInfo(nextTagMatch.attrs);
2870
2877
  } else {
2871
2878
  nextTagMatch = nextHtml.match(endTag);
2872
2879
  if (nextTagMatch) {
2873
2880
  nextTag = '/' + nextTagMatch[1];
2881
+ nextAttrs = [];
2874
2882
  } else {
2875
2883
  nextTag = '';
2884
+ nextAttrs = [];
2876
2885
  }
2877
2886
  }
2878
2887
 
2879
2888
  if (handler.chars) {
2880
- await handler.chars(text, prevTag, nextTag);
2889
+ await handler.chars(text, prevTag, nextTag, prevAttrs, nextAttrs);
2881
2890
  }
2882
2891
  prevTag = '';
2892
+ prevAttrs = [];
2883
2893
  } else {
2884
2894
  const stackedTag = lastTag.toLowerCase();
2885
2895
  // Use pre-compiled regex for common tags (`script`, `style`, `noscript`) to avoid regex creation overhead
@@ -2902,7 +2912,7 @@ class HTMLParser {
2902
2912
  } else {
2903
2913
  // No closing tag found; to avoid infinite loop, break similarly to previous behavior
2904
2914
  if (handler.continueOnParseError && handler.chars && html) {
2905
- await handler.chars(html[0], prevTag, '');
2915
+ await handler.chars(html[0], prevTag, '', prevAttrs, []);
2906
2916
  advance(1);
2907
2917
  } else {
2908
2918
  break;
@@ -2914,10 +2924,11 @@ class HTMLParser {
2914
2924
  if (handler.continueOnParseError) {
2915
2925
  // Skip the problematic character and continue
2916
2926
  if (handler.chars) {
2917
- await handler.chars(fullHtml[pos], prevTag, '');
2927
+ await handler.chars(fullHtml[pos], prevTag, '', prevAttrs, []);
2918
2928
  }
2919
2929
  advance(1);
2920
2930
  prevTag = '';
2931
+ prevAttrs = [];
2921
2932
  continue;
2922
2933
  }
2923
2934
  const loc = getLineColumn(pos);
@@ -2936,6 +2947,23 @@ class HTMLParser {
2936
2947
  await parseEndTag();
2937
2948
  }
2938
2949
 
2950
+ // Helper to extract minimal attribute info (name/value pairs) from raw attribute matches
2951
+ // Used for whitespace collapsing logic—doesn’t need full processing
2952
+ function extractAttrInfo(rawAttrs) {
2953
+ if (!rawAttrs || !rawAttrs.length) return [];
2954
+
2955
+ const numCustomParts = handler.customAttrSurround ? handler.customAttrSurround.length * NCP : 0;
2956
+ const baseIndex = 1 + numCustomParts;
2957
+
2958
+ return rawAttrs.map(args => {
2959
+ // Extract attribute name (always at `baseIndex`)
2960
+ const name = args[baseIndex];
2961
+ // Extract value from double-quoted (`baseIndex + 2`), single-quoted (`baseIndex + 3`), or unquoted (`baseIndex + 4`)
2962
+ const value = args[baseIndex + 2] ?? args[baseIndex + 3] ?? args[baseIndex + 4];
2963
+ return { name: name?.toLowerCase(), value };
2964
+ }).filter(attr => attr.name); // Filter out invalid entries
2965
+ }
2966
+
2939
2967
  function parseStartTag(input) {
2940
2968
  const start = input.match(startTagOpen);
2941
2969
  if (start) {
@@ -2948,7 +2976,7 @@ class HTMLParser {
2948
2976
  input = input.slice(consumed);
2949
2977
  let end, attr;
2950
2978
 
2951
- // Safety limit: max length of input to check for attributes
2979
+ // Safety limit: Max length of input to check for attributes
2952
2980
  // Protects against catastrophic backtracking on massive attribute values
2953
2981
  const MAX_ATTR_PARSE_LENGTH = 20000; // 20 KB should be enough for any reasonable tag
2954
2982
 
@@ -3048,7 +3076,7 @@ class HTMLParser {
3048
3076
  }
3049
3077
 
3050
3078
  async function parseEndTagAt(pos) {
3051
- // Close all open elements up to pos (mirrors parseEndTags core branch)
3079
+ // Close all open elements up to `pos` (mirrors `parseEndTag`’s core branch)
3052
3080
  for (let i = stack.length - 1; i >= pos; i--) {
3053
3081
  if (handler.end) {
3054
3082
  await handler.end(stack[i].tag, stack[i].attrs, true);
@@ -3116,7 +3144,7 @@ class HTMLParser {
3116
3144
  const attrs = match.attrs.map(function (args) {
3117
3145
  let name, value, customOpen, customClose, customAssign, quote;
3118
3146
 
3119
- // Hackish workaround for FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
3147
+ // Hackish workaround for Firefox bug, https://bugzilla.mozilla.org/show_bug.cgi?id=369778
3120
3148
  if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
3121
3149
  if (args[3] === '') { delete args[3]; }
3122
3150
  if (args[4] === '') { delete args[4]; }
@@ -3173,6 +3201,9 @@ class HTMLParser {
3173
3201
  unarySlash = '';
3174
3202
  }
3175
3203
 
3204
+ // Store attributes for `prevAttrs` tracking (used in whitespace collapsing)
3205
+ prevAttrs = attrs;
3206
+
3176
3207
  if (handler.start) {
3177
3208
  await handler.start(tagName, attrs, unary, unarySlash);
3178
3209
  }
@@ -3268,7 +3299,7 @@ class Sorter {
3268
3299
 
3269
3300
  class TokenChain {
3270
3301
  constructor() {
3271
- // Use Map instead of object properties for better performance
3302
+ // Use map instead of object properties for better performance
3272
3303
  this.map = new Map();
3273
3304
  }
3274
3305
 
@@ -3285,7 +3316,7 @@ class TokenChain {
3285
3316
  const sorter = new Sorter();
3286
3317
  sorter.sorterMap = new Map();
3287
3318
 
3288
- // Convert Map entries to array and sort by frequency (descending) then alphabetically
3319
+ // Convert map entries to array and sort by frequency (descending), then alphabetically
3289
3320
  const entries = Array.from(this.map.entries()).sort((a, b) => {
3290
3321
  const m = a[1].arrays.length;
3291
3322
  const n = b[1].arrays.length;
@@ -3334,11 +3365,11 @@ class TokenChain {
3334
3365
  }
3335
3366
 
3336
3367
  /**
3337
- * Preset configurations for HTML Minifier Next
3368
+ * Preset configurations
3338
3369
  *
3339
3370
  * Presets provide curated option sets for common use cases:
3340
- * - conservative: Safe minification suitable for most projects
3341
- * - comprehensive: Aggressive minification for maximum file size reduction
3371
+ * - `conservative`: Safe minification suitable for most projects
3372
+ * - `comprehensive`: Aggressive minification for maximum file size reduction
3342
3373
  */
3343
3374
 
3344
3375
  const presets = {
@@ -3384,7 +3415,7 @@ const presets = {
3384
3415
 
3385
3416
  /**
3386
3417
  * Get preset configuration by name
3387
- * @param {string} name - Preset name ('conservative' or 'comprehensive')
3418
+ * @param {string} name - Preset name (conservative or comprehensive)
3388
3419
  * @returns {object|null} Preset options object or null if not found
3389
3420
  */
3390
3421
  function getPreset(name) {
@@ -3483,7 +3514,7 @@ async function replaceAsync(str, regex, asyncFn) {
3483
3514
  return str.replace(regex, () => data.shift());
3484
3515
  }
3485
3516
 
3486
- // RegExp patterns (to avoid repeated allocations in hot paths)
3517
+ // Regex patterns (to avoid repeated allocations in hot paths)
3487
3518
 
3488
3519
  const RE_WS_START = /^[ \n\r\t\f]+/;
3489
3520
  const RE_WS_END = /[ \n\r\t\f]+$/;
@@ -3504,7 +3535,7 @@ const RE_ATTR_WS_COLLAPSE = /[ \n\r\t\f]+/g;
3504
3535
  const RE_ATTR_WS_TRIM = /^[ \n\r\t\f]+|[ \n\r\t\f]+$/g;
3505
3536
  const RE_NUMERIC_VALUE = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
3506
3537
 
3507
- // Inline element Sets for whitespace handling
3538
+ // Inline element sets for whitespace handling
3508
3539
 
3509
3540
  // Non-empty elements that will maintain whitespace around them
3510
3541
  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']);
@@ -3515,6 +3546,9 @@ const inlineElementsToKeepWhitespaceWithin = new Set(['a', 'abbr', 'acronym', 'b
3515
3546
  // Elements that will always maintain whitespace around them
3516
3547
  const inlineElementsToKeepWhitespace = new Set(['comment', 'img', 'input', 'wbr']);
3517
3548
 
3549
+ // Form control elements (for conditional whitespace collapsing)
3550
+ const formControlElements = new Set(['input', 'button', 'select', 'textarea', 'output', 'meter', 'progress']);
3551
+
3518
3552
  // Default attribute values
3519
3553
 
3520
3554
  // Default attribute values (could apply to any element)
@@ -3554,14 +3588,17 @@ const tagDefaults = {
3554
3588
  // Script MIME types
3555
3589
 
3556
3590
  // https://mathiasbynens.be/demo/javascript-mime-type
3557
- // https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type
3591
+ // https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script
3558
3592
  const executableScriptsMimetypes = new Set([
3559
3593
  'text/javascript',
3594
+ 'text/x-javascript',
3560
3595
  'text/ecmascript',
3596
+ 'text/x-ecmascript',
3561
3597
  'text/jscript',
3562
3598
  'application/javascript',
3563
3599
  'application/x-javascript',
3564
3600
  'application/ecmascript',
3601
+ 'application/x-ecmascript',
3565
3602
  'module'
3566
3603
  ]);
3567
3604
 
@@ -3569,15 +3606,15 @@ const keepScriptsMimetypes = new Set([
3569
3606
  'module'
3570
3607
  ]);
3571
3608
 
3572
- // Boolean attribute Sets
3609
+ // Boolean attribute sets
3573
3610
 
3574
3611
  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']);
3575
3612
 
3576
3613
  const isBooleanValue = new Set(['true', 'false']);
3577
3614
 
3578
- // `srcset` tags
3615
+ // `srcset` elements
3579
3616
 
3580
- const srcsetTags = new Set(['img', 'source']);
3617
+ const srcsetElements = new Set(['img', 'source']);
3581
3618
 
3582
3619
  // JSON script types
3583
3620
 
@@ -3593,7 +3630,7 @@ const jsonScriptTypes = new Set([
3593
3630
  'speculationrules',
3594
3631
  ]);
3595
3632
 
3596
- // Tag omission rules and element Sets
3633
+ // Tag omission rules and element sets
3597
3634
 
3598
3635
  // Tag omission rules from https://html.spec.whatwg.org/multipage/syntax.html#optional-tags with the following extensions:
3599
3636
  // - retain `<body>` if followed by `<noscript>`
@@ -3604,35 +3641,35 @@ const optionalStartTags = new Set(['html', 'head', 'body', 'colgroup', 'tbody'])
3604
3641
 
3605
3642
  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']);
3606
3643
 
3607
- const headerTags = new Set(['meta', 'link', 'script', 'style', 'template', 'noscript']);
3644
+ const headerElements = new Set(['meta', 'link', 'script', 'style', 'template', 'noscript']);
3608
3645
 
3609
- const descriptionTags = new Set(['dt', 'dd']);
3646
+ const descriptionElements = new Set(['dt', 'dd']);
3610
3647
 
3611
- 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']);
3648
+ 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']);
3612
3649
 
3613
- const pInlineTags = new Set(['a', 'audio', 'del', 'ins', 'map', 'noscript', 'video']);
3650
+ const pInlineElements = new Set(['a', 'audio', 'del', 'ins', 'map', 'noscript', 'video']);
3614
3651
 
3615
3652
  const rubyEndTagOmission = new Set(['rb', 'rt', 'rtc', 'rp']); // `</rb>`, `</rt>`, `</rp>` can be omitted if followed by `<rb>`, `<rt>`, `<rtc>`, or `<rp>`
3616
3653
 
3617
3654
  const rubyRtcEndTagOmission = new Set(['rb', 'rtc']); // `</rtc>` can be omitted if followed by `<rb>` or `<rtc>` (not `<rt>` or `<rp>`)
3618
3655
 
3619
- const optionTag = new Set(['option', 'optgroup']);
3656
+ const optionElements = new Set(['option', 'optgroup']);
3620
3657
 
3621
- const tableContentTags = new Set(['tbody', 'tfoot']);
3658
+ const tableContentElements = new Set(['tbody', 'tfoot']);
3622
3659
 
3623
- const tableSectionTags = new Set(['thead', 'tbody', 'tfoot']);
3660
+ const tableSectionElements = new Set(['thead', 'tbody', 'tfoot']);
3624
3661
 
3625
- const cellTags = new Set(['td', 'th']);
3662
+ const cellElements = new Set(['td', 'th']);
3626
3663
 
3627
- const topLevelTags = new Set(['html', 'head', 'body']);
3664
+ const topLevelElements = new Set(['html', 'head', 'body']);
3628
3665
 
3629
- const compactTags = new Set(['html', 'body']);
3666
+ const compactElements = new Set(['html', 'body']);
3630
3667
 
3631
- const looseTags = new Set(['head', 'colgroup', 'caption']);
3668
+ const looseElements = new Set(['head', 'colgroup', 'caption']);
3632
3669
 
3633
- const trailingTags = new Set(['dt', 'thead']);
3670
+ const trailingElements = new Set(['dt', 'thead']);
3634
3671
 
3635
- 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']);
3672
+ 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']);
3636
3673
 
3637
3674
  // Empty attribute regex
3638
3675
 
@@ -3642,7 +3679,7 @@ const reEmptyAttribute = new RegExp(
3642
3679
 
3643
3680
  // Special content elements
3644
3681
 
3645
- const specialContentTags = new Set(['script', 'style']);
3682
+ const specialContentElements = new Set(['script', 'style']);
3646
3683
 
3647
3684
  // Imports
3648
3685
 
@@ -3706,7 +3743,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
3706
3743
  }
3707
3744
 
3708
3745
  if (trimLeft) {
3709
- // Non-breaking space is specifically handled inside the replacer function
3746
+ // No-break space is specifically handled inside the replacer function
3710
3747
  str = str.replace(/^[ \n\r\t\f\xA0]+/, function (spaces) {
3711
3748
  const conservative = !lineBreakBefore && options.conservativeCollapse;
3712
3749
  if (conservative && spaces === '\t') {
@@ -3717,7 +3754,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
3717
3754
  }
3718
3755
 
3719
3756
  if (trimRight) {
3720
- // Non-breaking space is specifically handled inside the replacer function
3757
+ // No-break space is specifically handled inside the replacer function
3721
3758
  str = str.replace(/[ \n\r\t\f\xA0]+$/, function (spaces) {
3722
3759
  const conservative = !lineBreakAfter && options.conservativeCollapse;
3723
3760
  if (conservative && spaces === '\t') {
@@ -3741,11 +3778,42 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
3741
3778
 
3742
3779
  // Collapse whitespace smartly based on surrounding tags
3743
3780
 
3744
- function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements, inlineTextSet) {
3781
+ function collapseWhitespaceSmart(str, prevTag, nextTag, prevAttrs, nextAttrs, options, inlineElements, inlineTextSet) {
3782
+ const prevTagName = prevTag && (prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag);
3783
+ const nextTagName = nextTag && (nextTag.charAt(0) === '/' ? nextTag.slice(1) : nextTag);
3784
+
3785
+ // Helper: Check if an input element has `type="hidden"`
3786
+ const isHiddenInput = (tagName, attrs) => {
3787
+ if (tagName !== 'input' || !attrs || !attrs.length) return false;
3788
+ const typeAttr = attrs.find(attr => attr.name === 'type');
3789
+ return typeAttr && typeAttr.value === 'hidden';
3790
+ };
3791
+
3792
+ // Check if prev/next are non-rendering (hidden) elements
3793
+ const prevIsHidden = isHiddenInput(prevTagName, prevAttrs);
3794
+ const nextIsHidden = isHiddenInput(nextTagName, nextAttrs);
3795
+
3745
3796
  let trimLeft = prevTag && !inlineElementsToKeepWhitespace.has(prevTag);
3797
+
3798
+ // Smart default behavior: Collapse space after non-rendering elements (`type="hidden"`)
3799
+ // This happens even in basic `collapseWhitespace` mode (safe optimization)
3800
+ if (!trimLeft && prevIsHidden && str && !/\S/.test(str)) {
3801
+ trimLeft = true;
3802
+ }
3803
+
3804
+ // Aggressive mode: Collapse between all form controls (pure whitespace only)
3805
+ const isPureWhitespace = str && !/\S/.test(str);
3806
+ if (!trimLeft && prevTagName && nextTagName &&
3807
+ options.collapseInlineTagWhitespace &&
3808
+ isPureWhitespace &&
3809
+ formControlElements.has(prevTagName) && formControlElements.has(nextTagName)) {
3810
+ trimLeft = true;
3811
+ }
3812
+
3746
3813
  if (trimLeft && !options.collapseInlineTagWhitespace) {
3747
3814
  trimLeft = prevTag.charAt(0) === '/' ? !inlineElements.has(prevTag.slice(1)) : !inlineTextSet.has(prevTag);
3748
3815
  }
3816
+
3749
3817
  // When `collapseInlineTagWhitespace` is enabled, still preserve whitespace around inline text elements
3750
3818
  if (trimLeft && options.collapseInlineTagWhitespace) {
3751
3819
  const tagName = prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag;
@@ -3753,10 +3821,26 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements,
3753
3821
  trimLeft = false;
3754
3822
  }
3755
3823
  }
3824
+
3756
3825
  let trimRight = nextTag && !inlineElementsToKeepWhitespace.has(nextTag);
3826
+
3827
+ // Smart default behavior: Collapse space before non-rendering elements (`type="hidden"`)
3828
+ if (!trimRight && nextIsHidden && str && !/\S/.test(str)) {
3829
+ trimRight = true;
3830
+ }
3831
+
3832
+ // Aggressive mode: Same as `trimLeft`
3833
+ if (!trimRight && prevTagName && nextTagName &&
3834
+ options.collapseInlineTagWhitespace &&
3835
+ isPureWhitespace &&
3836
+ formControlElements.has(prevTagName) && formControlElements.has(nextTagName)) {
3837
+ trimRight = true;
3838
+ }
3839
+
3757
3840
  if (trimRight && !options.collapseInlineTagWhitespace) {
3758
3841
  trimRight = nextTag.charAt(0) === '/' ? !inlineTextSet.has(nextTag.slice(1)) : !inlineElements.has(nextTag);
3759
3842
  }
3843
+
3760
3844
  // When `collapseInlineTagWhitespace` is enabled, still preserve whitespace around inline text elements
3761
3845
  if (trimRight && options.collapseInlineTagWhitespace) {
3762
3846
  const tagName = nextTag.charAt(0) === '/' ? nextTag.slice(1) : nextTag;
@@ -3764,6 +3848,7 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements,
3764
3848
  trimRight = false;
3765
3849
  }
3766
3850
  }
3851
+
3767
3852
  return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
3768
3853
  }
3769
3854
 
@@ -6314,7 +6399,6 @@ var RelateURL = /*@__PURE__*/getDefaultExportFromCjs(libExports);
6314
6399
 
6315
6400
  // Wrap CSS declarations for inline styles and media queries
6316
6401
  // This ensures proper context for CSS minification
6317
-
6318
6402
  function wrapCSS(text, type) {
6319
6403
  switch (type) {
6320
6404
  case 'inline':
@@ -6396,7 +6480,6 @@ async function processScript(text, options, currentAttrs, minifyHTML) {
6396
6480
 
6397
6481
  /**
6398
6482
  * Lightweight SVG optimizations:
6399
- *
6400
6483
  * - Numeric precision reduction for coordinates and path data
6401
6484
  * - Whitespace removal in attribute values (numeric sequences)
6402
6485
  * - Default attribute removal (safe, well-documented defaults)
@@ -6490,7 +6573,7 @@ function minifyNumber(num, precision = 3) {
6490
6573
  if (num === '1.0' || num === '1.00' || num === '1.000') return '1';
6491
6574
 
6492
6575
  // Check cache
6493
- // (Note: uses input string as key, so “0.0000” and “0.00000” create separate entries.
6576
+ // (Note: Uses input string as key, so “0.0000” and “0.00000” create separate entries.
6494
6577
  // This is intentional to avoid parsing overhead.
6495
6578
  // Real-world SVG files from export tools typically use consistent formats.)
6496
6579
  const cacheKey = `${num}:${precision}`;
@@ -6529,15 +6612,15 @@ function minifyPathData(pathData, precision = 3) {
6529
6612
 
6530
6613
  // Remove unnecessary spaces around path commands
6531
6614
  // Safe to remove space after a command letter when it’s followed by a number (which may be negative)
6532
- // M 10 20 → M10 20, L -5 -3 → L-5-3
6615
+ // `M 10 20``M10 20`, `L -5 -3``L-5-3`
6533
6616
  result = result.replace(/([MLHVCSQTAZmlhvcsqtaz])\s+(?=-?\d)/g, '$1');
6534
6617
 
6535
6618
  // Safe to remove space before command letter when preceded by a number
6536
- // 0 L → 0L, 20 M → 20M
6619
+ // `0 L``0L`, `20 M``20M`
6537
6620
  result = result.replace(/(\d)\s+([MLHVCSQTAZmlhvcsqtaz])/g, '$1$2');
6538
6621
 
6539
6622
  // Safe to remove space before negative number when preceded by a number
6540
- // 10 -20 → 10-20 (numbers are separated by the minus sign)
6623
+ // `10 -20``10-20` (numbers are separated by the minus sign)
6541
6624
  result = result.replace(/(\d)\s+(-\d)/g, '$1$2');
6542
6625
 
6543
6626
  return result;
@@ -6546,9 +6629,9 @@ function minifyPathData(pathData, precision = 3) {
6546
6629
  /**
6547
6630
  * Minify whitespace in numeric attribute values
6548
6631
  * Examples:
6549
- * "10 , 20" → "10,20"
6550
- * "translate( 10 20 )" → "translate(10 20)"
6551
- * "100, 10 40, 198" → "100,10 40,198"
6632
+ * - “10 , 20" → "10,20"
6633
+ * - "translate( 10 20 )" → "translate(10 20)"
6634
+ * - "100, 10 40, 198" → "100,10 40,198"
6552
6635
  *
6553
6636
  * @param {string} value - Attribute value to minify
6554
6637
  * @returns {string} Minified value
@@ -6581,8 +6664,7 @@ function minifyColor(color) {
6581
6664
 
6582
6665
  // Don’t process values that aren’t simple colors (preserve case-sensitive references)
6583
6666
  // `url(#id)`, `var(--name)`, `inherit`, `currentColor`, etc.
6584
- if (trimmed.includes('url(') || trimmed.includes('var(') ||
6585
- trimmed === 'inherit' || trimmed === 'currentColor') {
6667
+ if (trimmed.includes('url(') || trimmed.includes('var(') || trimmed === 'inherit' || trimmed === 'currentColor') {
6586
6668
  return trimmed;
6587
6669
  }
6588
6670
 
@@ -6590,7 +6672,7 @@ function minifyColor(color) {
6590
6672
  const lower = trimmed.toLowerCase();
6591
6673
 
6592
6674
  // Shorten 6-digit hex to 3-digit when possible
6593
- // #aabbcc → #abc, #000000 → #000
6675
+ // `#aabbcc``#abc`, `#000000``#000`
6594
6676
  const hexMatch = lower.match(/^#([0-9a-f]{6})$/);
6595
6677
  if (hexMatch) {
6596
6678
  const hex = hexMatch[1];
@@ -6610,7 +6692,7 @@ function minifyColor(color) {
6610
6692
  return NAMED_COLORS[lower] || lower;
6611
6693
  }
6612
6694
 
6613
- // Convert rgb(255,255,255) to hex
6695
+ // Convert rgb() to hex
6614
6696
  const rgbMatch = lower.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
6615
6697
  if (rgbMatch) {
6616
6698
  const r = parseInt(rgbMatch[1], 10);
@@ -6641,14 +6723,14 @@ function minifyColor(color) {
6641
6723
  const NUMERIC_ATTRS = new Set([
6642
6724
  'd', // Path data
6643
6725
  'points', // Polygon/polyline points
6644
- 'viewBox', // viewBox coordinates
6726
+ 'viewBox', // `viewBox` coordinates
6645
6727
  'transform', // Transform functions
6646
6728
  'x', 'y', 'x1', 'y1', 'x2', 'y2', // Coordinates
6647
6729
  'cx', 'cy', 'r', 'rx', 'ry', // Circle/ellipse
6648
6730
  'width', 'height', // Dimensions
6649
6731
  'dx', 'dy', // Text offsets
6650
6732
  'offset', // Gradient offset
6651
- 'startOffset', // textPath
6733
+ 'startOffset', // `textPath`
6652
6734
  'pathLength', // Path length
6653
6735
  'stdDeviation', // Filter params
6654
6736
  'baseFrequency', // Turbulence
@@ -6832,8 +6914,8 @@ function shouldMinifyInnerHTML(options) {
6832
6914
  /**
6833
6915
  * @param {Partial<MinifierOptions>} inputOptions - User-provided options
6834
6916
  * @param {Object} deps - Dependencies from htmlminifier.js
6835
- * @param {Function} deps.getLightningCSS - Function to lazily load lightningcss
6836
- * @param {Function} deps.getTerser - Function to lazily load terser
6917
+ * @param {Function} deps.getLightningCSS - Function to lazily load Lightning CSS
6918
+ * @param {Function} deps.getTerser - Function to lazily load Terser
6837
6919
  * @param {Function} deps.getSwc - Function to lazily load @swc/core
6838
6920
  * @param {LRU} deps.cssMinifyCache - CSS minification cache
6839
6921
  * @param {LRU} deps.jsMinifyCache - JS minification cache
@@ -6870,7 +6952,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
6870
6952
  if (typeof value === 'string') {
6871
6953
  return new RegExp(value.replace(/^\/(.*)\/$/, '$1'));
6872
6954
  }
6873
- return value; // Already a RegExp or other type
6955
+ return value; // Already a RegExp or another type
6874
6956
  };
6875
6957
 
6876
6958
  const parseRegExpArray = (arr) => {
@@ -7007,7 +7089,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
7007
7089
  // Validate engine
7008
7090
  const supportedEngines = ['terser', 'swc'];
7009
7091
  if (!supportedEngines.includes(engine)) {
7010
- throw new Error(`Unsupported JS minifier engine: "${engine}". Supported engines: ${supportedEngines.join(', ')}`);
7092
+ throw new Error(`Unsupported JS minifier engine: “${engine}”. Supported engines: ${supportedEngines.join(', ')}`);
7011
7093
  }
7012
7094
 
7013
7095
  // Extract engine-specific options (excluding `engine` field itself)
@@ -7114,14 +7196,14 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
7114
7196
  relateUrlOptions = {};
7115
7197
  }
7116
7198
 
7117
- // Cache RelateURL instance for reuse (expensive to create)
7199
+ // Cache relateurl instance for reuse (expensive to create)
7118
7200
  const relateUrlInstance = new RelateURL(relateUrlOptions.site || '', relateUrlOptions);
7119
7201
 
7120
7202
  // Create instance-specific cache (results depend on site configuration)
7121
7203
  const instanceCache = urlMinifyCache ? new (urlMinifyCache.constructor)(500) : null;
7122
7204
 
7123
7205
  options.minifyURLs = function (text) {
7124
- // Fast-path: Skip if text doesn't look like a URL that needs processing
7206
+ // Fast-path: Skip if text doesnt look like a URL that needs processing
7125
7207
  // Only process if contains URL-like characters (`/`, `:`, `#`, `?`) or spaces that need encoding
7126
7208
  if (!/[/:?#\s]/.test(text)) {
7127
7209
  return text;
@@ -7153,17 +7235,17 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
7153
7235
  };
7154
7236
  } else if (key === 'minifySVG') {
7155
7237
  // Process SVG minification options
7156
- // Unlike minifyCSS/minifyJS, this is a simple options object, not a function
7238
+ // Unlike `minifyCSS`/`minifyJS`, this is a simple options object, not a function
7157
7239
  // The actual minification is applied inline during attribute processing
7158
7240
  options.minifySVG = getSVGMinifierOptions(option);
7159
7241
  } else if (key === 'customAttrCollapse') {
7160
- // Single RegExp pattern
7242
+ // Single regex pattern
7161
7243
  options[key] = parseRegExp(option);
7162
7244
  } else if (key === 'customAttrSurround') {
7163
7245
  // Nested array of RegExp pairs: `[[openRegExp, closeRegExp], …]`
7164
7246
  options[key] = parseNestedRegExpArray(option);
7165
7247
  } else if (['customAttrAssign', 'customEventAttributes', 'ignoreCustomComments', 'ignoreCustomFragments'].includes(key)) {
7166
- // Array of RegExp patterns
7248
+ // Array of regex patterns
7167
7249
  options[key] = parseRegExpArray(option);
7168
7250
  } else {
7169
7251
  options[key] = option;
@@ -7228,8 +7310,7 @@ function isAttributeRedundant(tag, attrName, attrValue, attrs) {
7228
7310
  const tagHasDefaults = tag in tagDefaults;
7229
7311
 
7230
7312
  // Check for legacy attribute rules (element- and attribute-specific)
7231
- const isLegacyAttr = (tag === 'script' && (attrName === 'language' || attrName === 'charset')) ||
7232
- (tag === 'a' && attrName === 'name');
7313
+ const isLegacyAttr = (tag === 'script' && (attrName === 'language' || attrName === 'charset')) || (tag === 'a' && attrName === 'name');
7233
7314
 
7234
7315
  // If none of these conditions apply, attribute cannot be redundant
7235
7316
  if (!hasGeneralDefault && !tagHasDefaults && !isLegacyAttr) {
@@ -7287,7 +7368,7 @@ function isStyleLinkTypeAttribute(attrValue = '') {
7287
7368
  return attrValue === '' || attrValue === 'text/css';
7288
7369
  }
7289
7370
 
7290
- function isStyleSheet(tag, attrs) {
7371
+ function isStyleElement(tag, attrs) {
7291
7372
  if (tag !== 'style') {
7292
7373
  return false;
7293
7374
  }
@@ -7344,11 +7425,11 @@ function isLinkType(tag, attrs, value) {
7344
7425
  }
7345
7426
 
7346
7427
  function isMediaQuery(tag, attrs, attrName) {
7347
- return attrName === 'media' && (isLinkType(tag, attrs, 'stylesheet') || isStyleSheet(tag, attrs));
7428
+ return attrName === 'media' && (isLinkType(tag, attrs, 'stylesheet') || isStyleElement(tag, attrs));
7348
7429
  }
7349
7430
 
7350
7431
  function isSrcset(attrName, tag) {
7351
- return attrName === 'srcset' && srcsetTags.has(tag);
7432
+ return attrName === 'srcset' && srcsetElements.has(tag);
7352
7433
  }
7353
7434
 
7354
7435
  function isMetaViewport(tag, attrs) {
@@ -7356,7 +7437,7 @@ function isMetaViewport(tag, attrs) {
7356
7437
  return false;
7357
7438
  }
7358
7439
  for (let i = 0, len = attrs.length; i < len; i++) {
7359
- if (attrs[i].name === 'name' && attrs[i].value === 'viewport') {
7440
+ if (attrs[i].name.toLowerCase() === 'name' && attrs[i].value.toLowerCase() === 'viewport') {
7360
7441
  return true;
7361
7442
  }
7362
7443
  }
@@ -7376,7 +7457,7 @@ function isContentSecurityPolicy(tag, attrs) {
7376
7457
  }
7377
7458
 
7378
7459
  function canDeleteEmptyAttribute(tag, attrName, attrValue, options) {
7379
- const isValueEmpty = !attrValue || /^\s*$/.test(attrValue);
7460
+ const isValueEmpty = !attrValue || attrValue.trim() === '';
7380
7461
  if (!isValueEmpty) {
7381
7462
  return false;
7382
7463
  }
@@ -7399,7 +7480,7 @@ function hasAttrName(name, attrs) {
7399
7480
 
7400
7481
  async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTMLSelf) {
7401
7482
  // Apply early whitespace normalization if enabled
7402
- // Preserves special spaces (non-breaking space, hair space, etc.) for consistency with `collapseWhitespace`
7483
+ // Preserves special spaces (no-break space, hair space, etc.) for consistency with `collapseWhitespace`
7403
7484
  if (options.collapseAttributeWhitespace) {
7404
7485
  // Fast path: Only process if whitespace exists (avoids regex overhead on clean values)
7405
7486
  if (RE_ATTR_WS_CHECK.test(attrValue)) {
@@ -7455,7 +7536,7 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
7455
7536
  try {
7456
7537
  attrValue = await options.minifyCSS(attrValue, 'inline');
7457
7538
  // After minification, check if CSS consists entirely of invalid properties (no values)
7458
- // E.g., `color:` or `margin:;padding:` should be treated as empty
7539
+ // I.e., `color:` or `margin:;padding:` should be treated as empty
7459
7540
  if (attrValue && /^(?:[a-z-]+:\s*;?\s*)+$/i.test(attrValue)) {
7460
7541
  attrValue = '';
7461
7542
  }
@@ -7575,13 +7656,13 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
7575
7656
  }
7576
7657
 
7577
7658
  if ((options.removeRedundantAttributes &&
7578
- isAttributeRedundant(tag, attrName, attrValue, attrs)) ||
7579
- (options.removeScriptTypeAttributes && tag === 'script' &&
7580
- attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue)) ||
7581
- (options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
7582
- attrName === 'type' && isStyleLinkTypeAttribute(attrValue)) ||
7583
- (options.insideSVG && options.minifySVG &&
7584
- shouldRemoveSVGAttribute(tag, attrName, attrValue, options.minifySVG))) {
7659
+ isAttributeRedundant(tag, attrName, attrValue, attrs)) ||
7660
+ (options.removeScriptTypeAttributes && tag === 'script' &&
7661
+ attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue)) ||
7662
+ (options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
7663
+ attrName === 'type' && isStyleLinkTypeAttribute(attrValue)) ||
7664
+ (options.insideSVG && options.minifySVG &&
7665
+ shouldRemoveSVGAttribute(tag, attrName, attrValue, options.minifySVG))) {
7585
7666
  return;
7586
7667
  }
7587
7668
 
@@ -7590,7 +7671,7 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
7590
7671
  }
7591
7672
 
7592
7673
  if (options.removeEmptyAttributes &&
7593
- canDeleteEmptyAttribute(tag, attrName, attrValue, options)) {
7674
+ canDeleteEmptyAttribute(tag, attrName, attrValue, options)) {
7594
7675
  return;
7595
7676
  }
7596
7677
 
@@ -7613,19 +7694,35 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
7613
7694
  let attrFragment;
7614
7695
  let emittedAttrValue;
7615
7696
 
7616
- if (typeof attrValue !== 'undefined' && (!options.removeAttributeQuotes ||
7617
- attrValue.indexOf(uidAttr) !== -1 || !canRemoveAttributeQuotes(attrValue))) {
7697
+ // Determine if we need to add/keep quotes
7698
+ const shouldAddQuotes = typeof attrValue !== 'undefined' && (
7699
+ // If `removeAttributeQuotes` is enabled, add quotes only if they can’t be removed
7700
+ (options.removeAttributeQuotes && (attrValue.indexOf(uidAttr) !== -1 || !canRemoveAttributeQuotes(attrValue))) ||
7701
+ // If `removeAttributeQuotes` is not enabled, preserve original quote style or add quotes if value requires them
7702
+ (!options.removeAttributeQuotes && (attrQuote !== '' || !canRemoveAttributeQuotes(attrValue) ||
7703
+ // Special case: With `removeTagWhitespace`, unquoted values that aren’t last will have space added,
7704
+ // which can create ambiguous/invalid HTML—add quotes to be safe
7705
+ (options.removeTagWhitespace && attrQuote === '' && !isLast)))
7706
+ );
7707
+
7708
+ if (shouldAddQuotes) {
7618
7709
  // Determine the appropriate quote character
7619
7710
  if (!options.preventAttributesEscaping) {
7620
- // Normal mode: choose quotes and escape
7621
- attrQuote = chooseAttributeQuote(attrValue, options);
7711
+ // Normal mode: Choose optimal quote type to minimize escaping
7712
+ // unless we’re preserving original quotes and they don’t need escaping
7713
+ const needsEscaping = (attrQuote === '"' && attrValue.indexOf('"') !== -1) || (attrQuote === "'" && attrValue.indexOf("'") !== -1);
7714
+
7715
+ if (options.removeAttributeQuotes || typeof options.quoteCharacter !== 'undefined' || needsEscaping || attrQuote === '') {
7716
+ attrQuote = chooseAttributeQuote(attrValue, options);
7717
+ }
7718
+
7622
7719
  if (attrQuote === '"') {
7623
7720
  attrValue = attrValue.replace(/"/g, '&#34;');
7624
7721
  } else {
7625
7722
  attrValue = attrValue.replace(/'/g, '&#39;');
7626
7723
  }
7627
7724
  } else {
7628
- // `preventAttributesEscaping` mode: choose safe quotes but don't escape
7725
+ // `preventAttributesEscaping` mode: Choose safe quotes but don't escape
7629
7726
  // except when both quote types are present—then escape to prevent invalid HTML
7630
7727
  const hasDoubleQuote = attrValue.indexOf('"') !== -1;
7631
7728
  const hasSingleQuote = attrValue.indexOf("'") !== -1;
@@ -7644,8 +7741,18 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
7644
7741
  attrQuote = "'";
7645
7742
  } else if (attrQuote === "'" && hasSingleQuote && !hasDoubleQuote) {
7646
7743
  attrQuote = '"';
7647
- // Fallback for invalid/unsupported attrQuote values (not `"`, `'`, or empty string): Choose safe default based on value content
7648
- } else if (attrQuote !== '"' && attrQuote !== "'" && attrQuote !== '') {
7744
+ // If no quote character yet (empty string), choose based on content
7745
+ } else if (attrQuote === '') {
7746
+ if (hasSingleQuote && !hasDoubleQuote) {
7747
+ attrQuote = '"';
7748
+ } else if (hasDoubleQuote && !hasSingleQuote) {
7749
+ attrQuote = "'";
7750
+ } else {
7751
+ attrQuote = '"';
7752
+ }
7753
+ // Fallback for invalid/unsupported attrQuote values (not `"`, `'`, or empty string):
7754
+ // Choose safe default based on value content
7755
+ } else if (attrQuote !== '"' && attrQuote !== "'") {
7649
7756
  if (hasSingleQuote && !hasDoubleQuote) {
7650
7757
  attrQuote = '"';
7651
7758
  } else if (hasDoubleQuote && !hasSingleQuote) {
@@ -7655,7 +7762,22 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
7655
7762
  }
7656
7763
  }
7657
7764
  } else {
7658
- attrQuote = options.quoteCharacter === '\'' ? '\'' : '"';
7765
+ // `quoteCharacter` is explicitly set
7766
+ const preferredQuote = options.quoteCharacter === '\'' ? '\'' : '"';
7767
+ // Safety check: If the preferred quote conflicts with value content, switch to the opposite quote
7768
+ if ((preferredQuote === '"' && hasDoubleQuote && !hasSingleQuote) || (preferredQuote === "'" && hasSingleQuote && !hasDoubleQuote)) {
7769
+ attrQuote = preferredQuote === '"' ? "'" : '"';
7770
+ } else if ((preferredQuote === '"' && hasDoubleQuote && hasSingleQuote) || (preferredQuote === "'" && hasSingleQuote && hasDoubleQuote)) {
7771
+ // Both quote types present: Fall back to escaping despite `preventAttributesEscaping`
7772
+ attrQuote = preferredQuote;
7773
+ if (attrQuote === '"') {
7774
+ attrValue = attrValue.replace(/"/g, '&#34;');
7775
+ } else {
7776
+ attrValue = attrValue.replace(/'/g, '&#39;');
7777
+ }
7778
+ } else {
7779
+ attrQuote = preferredQuote;
7780
+ }
7659
7781
  }
7660
7782
  }
7661
7783
  emittedAttrValue = attrQuote + attrValue + attrQuote;
@@ -7663,15 +7785,17 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
7663
7785
  emittedAttrValue += ' ';
7664
7786
  }
7665
7787
  } else if (isLast && !hasUnarySlash) {
7666
- // Last attribute in a non-self-closing tag: no space needed
7788
+ // Last attribute in a non-self-closing tag:
7789
+ // No space needed
7667
7790
  emittedAttrValue = attrValue;
7668
7791
  } else {
7669
- // Not last attribute, or is a self-closing tag: add space
7792
+ // Not last attribute, or is a self-closing tag:
7793
+ // Unquoted values must have space after them to delimit from next attribute
7670
7794
  emittedAttrValue = attrValue + ' ';
7671
7795
  }
7672
7796
 
7673
7797
  if (typeof attrValue === 'undefined' || (options.collapseBooleanAttributes &&
7674
- isBooleanAttribute(attrName.toLowerCase(), (attrValue || '').toLowerCase()))) {
7798
+ isBooleanAttribute(attrName.toLowerCase(), (attrValue || '').toLowerCase()))) {
7675
7799
  attrFragment = attrName;
7676
7800
  if (!isLast) {
7677
7801
  attrFragment += ' ';
@@ -7694,7 +7818,7 @@ function canRemoveParentTag(optionalStartTag, tag) {
7694
7818
  case 'head':
7695
7819
  return true;
7696
7820
  case 'body':
7697
- return !headerTags.has(tag);
7821
+ return !headerElements.has(tag);
7698
7822
  case 'colgroup':
7699
7823
  return tag === 'col';
7700
7824
  case 'tbody':
@@ -7708,7 +7832,7 @@ function isStartTagMandatory(optionalEndTag, tag) {
7708
7832
  case 'colgroup':
7709
7833
  return optionalEndTag === 'colgroup';
7710
7834
  case 'tbody':
7711
- return tableSectionTags.has(optionalEndTag);
7835
+ return tableSectionElements.has(optionalEndTag);
7712
7836
  }
7713
7837
  return false;
7714
7838
  }
@@ -7727,9 +7851,9 @@ function canRemovePrecedingTag(optionalEndTag, tag) {
7727
7851
  return tag === optionalEndTag;
7728
7852
  case 'dt':
7729
7853
  case 'dd':
7730
- return descriptionTags.has(tag);
7854
+ return descriptionElements.has(tag);
7731
7855
  case 'p':
7732
- return pBlockTags.has(tag);
7856
+ return pBlockElements.has(tag);
7733
7857
  case 'rb':
7734
7858
  case 'rt':
7735
7859
  case 'rp':
@@ -7737,15 +7861,15 @@ function canRemovePrecedingTag(optionalEndTag, tag) {
7737
7861
  case 'rtc':
7738
7862
  return rubyRtcEndTagOmission.has(tag);
7739
7863
  case 'option':
7740
- return optionTag.has(tag);
7864
+ return optionElements.has(tag);
7741
7865
  case 'thead':
7742
7866
  case 'tbody':
7743
- return tableContentTags.has(tag);
7867
+ return tableContentElements.has(tag);
7744
7868
  case 'tfoot':
7745
7869
  return tag === 'tbody';
7746
7870
  case 'td':
7747
7871
  case 'th':
7748
- return cellTags.has(tag);
7872
+ return cellElements.has(tag);
7749
7873
  }
7750
7874
  return false;
7751
7875
  }
@@ -7848,7 +7972,7 @@ function parseRemoveEmptyElementsExcept(input, options) {
7848
7972
  if (typeof item === 'string') {
7849
7973
  const spec = parseElementSpec(item, options);
7850
7974
  if (!spec && options.log) {
7851
- options.log('Warning: Unable to parse “removeEmptyElementsExcept” specification: "' + item + '"');
7975
+ options.log('Warning: Unable to parse “removeEmptyElementsExcept” specification: ' + item + '');
7852
7976
  }
7853
7977
  return spec;
7854
7978
  }
@@ -8361,7 +8485,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
8361
8485
  }
8362
8486
 
8363
8487
  // Pre-compile regex patterns for reuse (performance optimization)
8364
- // These must be declared before scan() since scan uses them
8488
+ // These must be declared before `scan()` since scan uses them
8365
8489
  const whitespaceSplitPatternScan = /[ \t\n\f\r]+/;
8366
8490
  const whitespaceSplitPatternSort = /[ \n\f\r]+/;
8367
8491
 
@@ -8393,9 +8517,9 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
8393
8517
  chars: async function (text) {
8394
8518
  // Only recursively scan HTML content, not JSON-LD or other non-HTML script types
8395
8519
  // `scan()` is for analyzing HTML attribute order, not for parsing JSON
8396
- if (options.processScripts && specialContentTags.has(currentTag) &&
8397
- options.processScripts.indexOf(currentType) > -1 &&
8398
- currentType === 'text/html') {
8520
+ if (options.processScripts && specialContentElements.has(currentTag) &&
8521
+ options.processScripts.indexOf(currentType) > -1 &&
8522
+ currentType === 'text/html') {
8399
8523
  await scan(text);
8400
8524
  }
8401
8525
  },
@@ -8418,7 +8542,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
8418
8542
  // For the first pass, create a copy of options and disable aggressive minification.
8419
8543
  // Keep attribute transformations (like `removeStyleLinkTypeAttributes`) for accurate analysis.
8420
8544
  // This is safe because `createSortFns` is called before custom fragment UID markers (`uidAttr`) are added.
8421
- // Note: `htmlmin:ignore` UID markers (uidIgnore) already exist and are expanded for analysis.
8545
+ // Note: `htmlmin:ignore` UID markers (`uidIgnore`) already exist and are expanded for analysis.
8422
8546
  const firstPassOptions = Object.assign({}, options, {
8423
8547
  // Disable sorting for the analysis pass
8424
8548
  sortAttributes: false,
@@ -8437,7 +8561,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
8437
8561
  });
8438
8562
 
8439
8563
  // Temporarily enable `continueOnParseError` for the `scan()` function call below.
8440
- // Note: `firstPassOptions` already has `continueOnParseError: true` for the minifyHTML call.
8564
+ // Note: `firstPassOptions` already has `continueOnParseError: true` for the `minifyHTML` call.
8441
8565
  const originalContinueOnParseError = options.continueOnParseError;
8442
8566
  options.continueOnParseError = true;
8443
8567
 
@@ -8450,7 +8574,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
8450
8574
  : null;
8451
8575
 
8452
8576
  try {
8453
- // Expand UID tokens back to original content for frequency analysis
8577
+ // Expand UID tokens back to the original content for frequency analysis
8454
8578
  let expandedValue = value;
8455
8579
  if (uidReplacePattern) {
8456
8580
  expandedValue = value.replace(uidReplacePattern, function (match, index) {
@@ -8499,7 +8623,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
8499
8623
  attrOrderCache.set(cacheKey, sortedNames);
8500
8624
  }
8501
8625
 
8502
- // Apply the sorted order to attrs
8626
+ // Apply the sorted order to `attrs`
8503
8627
  const attrMap = Object.create(null);
8504
8628
  names.forEach(function (name, index) {
8505
8629
  (attrMap[name] || (attrMap[name] = [])).push(attrs[index]);
@@ -8586,7 +8710,7 @@ async function minifyHTML(value, options, partialMarkup) {
8586
8710
  const customElementsInput = options.inlineCustomElements ?? [];
8587
8711
  const customElementsArr = Array.isArray(customElementsInput) ? customElementsInput : Array.from(customElementsInput);
8588
8712
  const normalizedCustomElements = customElementsArr.map(name => options.name(name));
8589
- // Fast path: Reuse base Sets if no custom elements
8713
+ // Fast path: Reuse base sets if no custom elements
8590
8714
  const inlineTextSet = normalizedCustomElements.length
8591
8715
  ? new Set([...inlineElementsToKeepWhitespaceWithin, ...normalizedCustomElements])
8592
8716
  : inlineElementsToKeepWhitespaceWithin;
@@ -8606,7 +8730,7 @@ async function minifyHTML(value, options, partialMarkup) {
8606
8730
  }
8607
8731
 
8608
8732
  // Temporarily replace ignored chunks with comments, so that we don’t have to worry what’s there.
8609
- // For all we care there might be completely-horribly-broken-alien-non-html-emoj-cthulhu-filled content
8733
+ // For all we care there might be completely-horribly-broken-alien-non-html-emoji-cthulhu-filled content
8610
8734
  value = value.replace(/<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g, function (match, group1) {
8611
8735
  if (!uidIgnore) {
8612
8736
  uidIgnore = uniqueId(value);
@@ -8627,7 +8751,7 @@ async function minifyHTML(value, options, partialMarkup) {
8627
8751
  // Create sort functions after `htmlmin:ignore` processing but before custom fragment UID markers
8628
8752
  // This allows proper frequency analysis with access to ignored content via UID tokens
8629
8753
  if ((options.sortAttributes && typeof options.sortAttributes !== 'function') ||
8630
- (options.sortClassName && typeof options.sortClassName !== 'function')) {
8754
+ (options.sortClassName && typeof options.sortClassName !== 'function')) {
8631
8755
  await createSortFns(value, options, uidIgnore, null, ignoredMarkupChunks);
8632
8756
  }
8633
8757
 
@@ -8689,11 +8813,11 @@ async function minifyHTML(value, options, partialMarkup) {
8689
8813
  });
8690
8814
  }
8691
8815
 
8692
- function _canCollapseWhitespace(tag, attrs) {
8816
+ function canCollapseWhitespace$1(tag, attrs) {
8693
8817
  return options.canCollapseWhitespace(tag, attrs, canCollapseWhitespace);
8694
8818
  }
8695
8819
 
8696
- function _canTrimWhitespace(tag, attrs) {
8820
+ function canTrimWhitespace$1(tag, attrs) {
8697
8821
  return options.canTrimWhitespace(tag, attrs, canTrimWhitespace);
8698
8822
  }
8699
8823
 
@@ -8715,12 +8839,12 @@ async function minifyHTML(value, options, partialMarkup) {
8715
8839
 
8716
8840
  // Look for trailing whitespaces, bypass any inline tags
8717
8841
  function trimTrailingWhitespace(index, nextTag) {
8718
- for (let endTag = null; index >= 0 && _canTrimWhitespace(endTag); index--) {
8842
+ for (let endTag = null; index >= 0 && canTrimWhitespace$1(endTag); index--) {
8719
8843
  const str = buffer[index];
8720
8844
  const match = str.match(/^<\/([\w:-]+)>$/);
8721
8845
  if (match) {
8722
8846
  endTag = match[1];
8723
- } else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, options, inlineElements, inlineTextSet))) {
8847
+ } else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, [], [], options, inlineElements, inlineTextSet))) {
8724
8848
  break;
8725
8849
  }
8726
8850
  }
@@ -8769,10 +8893,10 @@ async function minifyHTML(value, options, partialMarkup) {
8769
8893
 
8770
8894
  let optional = options.removeOptionalTags;
8771
8895
  if (optional) {
8772
- const htmlTag = htmlTags.has(tag);
8896
+ const htmlTag = htmlElements.has(tag);
8773
8897
  // `<html>` may be omitted if first thing inside is not a comment
8774
8898
  // `<head>` may be omitted if first thing inside is an element
8775
- // `<body>` may be omitted if first thing inside is not space, comment, `<meta>`, `<link>`, `<script>`, <`style>`, or `<template>`
8899
+ // `<body>` may be omitted if first thing inside is not space, comment, `<meta>`, `<link>`, `<script>`, `<style>`, or `<template>`
8776
8900
  // `<colgroup>` may be omitted if first thing inside is `<col>`
8777
8901
  // `<tbody>` may be omitted if first thing inside is `<tr>`
8778
8902
  if (htmlTag && canRemoveParentTag(optionalStartTag, tag)) {
@@ -8789,16 +8913,16 @@ async function minifyHTML(value, options, partialMarkup) {
8789
8913
  optionalEndTag = '';
8790
8914
  }
8791
8915
 
8792
- // Set whitespace flags for nested tags (e.g., <code> within a <pre>)
8916
+ // Set whitespace flags for nested tags (e.g., `<code>` within a `<pre>`)
8793
8917
  if (options.collapseWhitespace) {
8794
8918
  if (!stackNoTrimWhitespace.length) {
8795
8919
  squashTrailingWhitespace(tag);
8796
8920
  }
8797
8921
  if (!unary) {
8798
- if (!_canTrimWhitespace(tag, attrs) || stackNoTrimWhitespace.length) {
8922
+ if (!canTrimWhitespace$1(tag, attrs) || stackNoTrimWhitespace.length) {
8799
8923
  stackNoTrimWhitespace.push(tag);
8800
8924
  }
8801
- if (!_canCollapseWhitespace(tag, attrs) || stackNoCollapseWhitespace.length) {
8925
+ if (!canCollapseWhitespace$1(tag, attrs) || stackNoCollapseWhitespace.length) {
8802
8926
  stackNoCollapseWhitespace.push(tag);
8803
8927
  }
8804
8928
  }
@@ -8854,7 +8978,7 @@ async function minifyHTML(value, options, partialMarkup) {
8854
8978
  squashTrailingWhitespace('/' + tag);
8855
8979
  }
8856
8980
  if (stackNoCollapseWhitespace.length &&
8857
- tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
8981
+ tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
8858
8982
  stackNoCollapseWhitespace.pop();
8859
8983
  }
8860
8984
  }
@@ -8867,7 +8991,7 @@ async function minifyHTML(value, options, partialMarkup) {
8867
8991
 
8868
8992
  if (options.removeOptionalTags) {
8869
8993
  // `<html>`, `<head>` or `<body>` may be omitted if the element is empty
8870
- if (isElementEmpty && topLevelTags.has(optionalStartTag)) {
8994
+ if (isElementEmpty && topLevelElements.has(optionalStartTag)) {
8871
8995
  removeStartTag();
8872
8996
  }
8873
8997
  optionalStartTag = '';
@@ -8875,7 +8999,7 @@ async function minifyHTML(value, options, partialMarkup) {
8875
8999
  // `</head>` may be omitted if not followed by space or comment
8876
9000
  // `</p>` may be omitted if no more content in non-`</a>` parent
8877
9001
  // except for `</dt>` or `</thead>`, end tags may be omitted if no more content in parent element
8878
- if (tag && optionalEndTag && !trailingTags.has(optionalEndTag) && (optionalEndTag !== 'p' || !pInlineTags.has(tag))) {
9002
+ if (tag && optionalEndTag && !trailingElements.has(optionalEndTag) && (optionalEndTag !== 'p' || !pInlineElements.has(tag))) {
8879
9003
  removeEndTag();
8880
9004
  }
8881
9005
  optionalEndTag = optionalEndTags.has(tag) ? tag : '';
@@ -8922,10 +9046,12 @@ async function minifyHTML(value, options, partialMarkup) {
8922
9046
  }
8923
9047
  }
8924
9048
  },
8925
- chars: async function (text, prevTag, nextTag) {
9049
+ chars: async function (text, prevTag, nextTag, prevAttrs, nextAttrs) {
8926
9050
  prevTag = prevTag === '' ? 'comment' : prevTag;
8927
9051
  nextTag = nextTag === '' ? 'comment' : nextTag;
8928
- if (options.decodeEntities && text && !specialContentTags.has(currentTag)) {
9052
+ prevAttrs = prevAttrs || [];
9053
+ nextAttrs = nextAttrs || [];
9054
+ if (options.decodeEntities && text && !specialContentElements.has(currentTag)) {
8929
9055
  if (text.indexOf('&') !== -1) {
8930
9056
  text = decodeHTML(text);
8931
9057
  }
@@ -8961,7 +9087,7 @@ async function minifyHTML(value, options, partialMarkup) {
8961
9087
  }
8962
9088
  }
8963
9089
  if (prevTag || nextTag) {
8964
- text = collapseWhitespaceSmart(text, prevTag, nextTag, options, inlineElements, inlineTextSet);
9090
+ text = collapseWhitespaceSmart(text, prevTag, nextTag, prevAttrs, nextAttrs, options, inlineElements, inlineTextSet);
8965
9091
  } else {
8966
9092
  text = collapseWhitespace(text, options, true, true);
8967
9093
  }
@@ -8973,13 +9099,13 @@ async function minifyHTML(value, options, partialMarkup) {
8973
9099
  text = collapseWhitespace(text, options, false, false, true);
8974
9100
  }
8975
9101
  }
8976
- if (specialContentTags.has(currentTag) && (options.processScripts || hasJsonScriptType(currentAttrs))) {
9102
+ if (specialContentElements.has(currentTag) && (options.processScripts || hasJsonScriptType(currentAttrs))) {
8977
9103
  text = await processScript(text, options, currentAttrs, minifyHTML);
8978
9104
  }
8979
9105
  if (isExecutableScript(currentTag, currentAttrs)) {
8980
9106
  text = await options.minifyJS(text);
8981
9107
  }
8982
- if (isStyleSheet(currentTag, currentAttrs)) {
9108
+ if (isStyleElement(currentTag, currentAttrs)) {
8983
9109
  text = await options.minifyCSS(text);
8984
9110
  }
8985
9111
  if (options.removeOptionalTags && text) {
@@ -8991,7 +9117,7 @@ async function minifyHTML(value, options, partialMarkup) {
8991
9117
  optionalStartTag = '';
8992
9118
  // `</html>` or `</body>` may be omitted if not followed by comment
8993
9119
  // `</head>`, `</colgroup>`, or `</caption>` may be omitted if not followed by space or comment
8994
- if (compactTags.has(optionalEndTag) || (looseTags.has(optionalEndTag) && !/^\s/.test(text))) {
9120
+ if (compactElements.has(optionalEndTag) || (looseElements.has(optionalEndTag) && !/^\s/.test(text))) {
8995
9121
  removeEndTag();
8996
9122
  }
8997
9123
  // Don’t reset optionalEndTag if text is only whitespace and will be collapsed (not conservatively)
@@ -9000,11 +9126,11 @@ async function minifyHTML(value, options, partialMarkup) {
9000
9126
  }
9001
9127
  }
9002
9128
  charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
9003
- if (options.decodeEntities && text && !specialContentTags.has(currentTag)) {
9129
+ if (options.decodeEntities && text && !specialContentElements.has(currentTag)) {
9004
9130
  // Escape any `&` symbols that start either:
9005
- // 1) a legacy named character reference (i.e., one that doesn’t end with `;`)
9131
+ // 1) a legacy-named character reference (i.e., one that doesn’t end with `;`)
9006
9132
  // 2) or any other character reference (i.e., one that does end with `;`)
9007
- // Note that `&` can be escaped as `&amp`, without the semi-colon.
9133
+ // Note that `&` can be escaped as `&amp`, without the semicolon.
9008
9134
  // https://mathiasbynens.be/notes/ambiguous-ampersands
9009
9135
  if (text.indexOf('&') !== -1) {
9010
9136
  text = text.replace(RE_LEGACY_ENTITIES, '&amp$1');
@@ -9069,7 +9195,7 @@ async function minifyHTML(value, options, partialMarkup) {
9069
9195
 
9070
9196
  // Only collapse whitespace if both blocks contain HTML (start with `<`)
9071
9197
  // Don’t collapse if either contains plain text, as that would change meaning
9072
- // Note: This check will match HTML comments (`<!-- … -->`), but the tag-name
9198
+ // Note: This check will match HTML comments (`<!-- … -->`), but the tag name
9073
9199
  // regex below requires starting with a letter, so comments are intentionally
9074
9200
  // excluded by the `currentTagMatch && prevTagMatch` guard
9075
9201
  if (currentContent && prevContent && /^\s*</.test(currentContent) && /^\s*</.test(prevContent)) {
@@ -9130,11 +9256,11 @@ async function minifyHTML(value, options, partialMarkup) {
9130
9256
  if (options.removeOptionalTags) {
9131
9257
  // `<html>` may be omitted if first thing inside is not a comment
9132
9258
  // `<head>` or `<body>` may be omitted if empty
9133
- if (topLevelTags.has(optionalStartTag)) {
9259
+ if (topLevelElements.has(optionalStartTag)) {
9134
9260
  removeStartTag();
9135
9261
  }
9136
9262
  // except for `</dt>` or `</thead>`, end tags may be omitted if no more content in parent element
9137
- if (optionalEndTag && !trailingTags.has(optionalEndTag)) {
9263
+ if (optionalEndTag && !trailingElements.has(optionalEndTag)) {
9138
9264
  removeEndTag();
9139
9265
  }
9140
9266
  }