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.
@@ -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 = {
@@ -3359,7 +3390,6 @@ const presets = {
3359
3390
  comprehensive: {
3360
3391
  caseSensitive: true,
3361
3392
  collapseBooleanAttributes: true,
3362
- collapseInlineTagWhitespace: true,
3363
3393
  collapseWhitespace: true,
3364
3394
  continueOnParseError: true,
3365
3395
  decodeEntities: true,
@@ -3384,7 +3414,7 @@ const presets = {
3384
3414
 
3385
3415
  /**
3386
3416
  * Get preset configuration by name
3387
- * @param {string} name - Preset name ('conservative' or 'comprehensive')
3417
+ * @param {string} name - Preset name (conservative or comprehensive)
3388
3418
  * @returns {object|null} Preset options object or null if not found
3389
3419
  */
3390
3420
  function getPreset(name) {
@@ -3483,7 +3513,7 @@ async function replaceAsync(str, regex, asyncFn) {
3483
3513
  return str.replace(regex, () => data.shift());
3484
3514
  }
3485
3515
 
3486
- // RegExp patterns (to avoid repeated allocations in hot paths)
3516
+ // Regex patterns (to avoid repeated allocations in hot paths)
3487
3517
 
3488
3518
  const RE_WS_START = /^[ \n\r\t\f]+/;
3489
3519
  const RE_WS_END = /[ \n\r\t\f]+$/;
@@ -3504,7 +3534,7 @@ const RE_ATTR_WS_COLLAPSE = /[ \n\r\t\f]+/g;
3504
3534
  const RE_ATTR_WS_TRIM = /^[ \n\r\t\f]+|[ \n\r\t\f]+$/g;
3505
3535
  const RE_NUMERIC_VALUE = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
3506
3536
 
3507
- // Inline element Sets for whitespace handling
3537
+ // Inline element sets for whitespace handling
3508
3538
 
3509
3539
  // Non-empty elements that will maintain whitespace around them
3510
3540
  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 +3545,9 @@ const inlineElementsToKeepWhitespaceWithin = new Set(['a', 'abbr', 'acronym', 'b
3515
3545
  // Elements that will always maintain whitespace around them
3516
3546
  const inlineElementsToKeepWhitespace = new Set(['comment', 'img', 'input', 'wbr']);
3517
3547
 
3548
+ // Form control elements (for conditional whitespace collapsing)
3549
+ const formControlElements = new Set(['input', 'button', 'select', 'textarea', 'output', 'meter', 'progress']);
3550
+
3518
3551
  // Default attribute values
3519
3552
 
3520
3553
  // Default attribute values (could apply to any element)
@@ -3554,14 +3587,17 @@ const tagDefaults = {
3554
3587
  // Script MIME types
3555
3588
 
3556
3589
  // https://mathiasbynens.be/demo/javascript-mime-type
3557
- // https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type
3590
+ // https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script
3558
3591
  const executableScriptsMimetypes = new Set([
3559
3592
  'text/javascript',
3593
+ 'text/x-javascript',
3560
3594
  'text/ecmascript',
3595
+ 'text/x-ecmascript',
3561
3596
  'text/jscript',
3562
3597
  'application/javascript',
3563
3598
  'application/x-javascript',
3564
3599
  'application/ecmascript',
3600
+ 'application/x-ecmascript',
3565
3601
  'module'
3566
3602
  ]);
3567
3603
 
@@ -3569,15 +3605,15 @@ const keepScriptsMimetypes = new Set([
3569
3605
  'module'
3570
3606
  ]);
3571
3607
 
3572
- // Boolean attribute Sets
3608
+ // Boolean attribute sets
3573
3609
 
3574
3610
  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
3611
 
3576
3612
  const isBooleanValue = new Set(['true', 'false']);
3577
3613
 
3578
- // `srcset` tags
3614
+ // `srcset` elements
3579
3615
 
3580
- const srcsetTags = new Set(['img', 'source']);
3616
+ const srcsetElements = new Set(['img', 'source']);
3581
3617
 
3582
3618
  // JSON script types
3583
3619
 
@@ -3593,7 +3629,7 @@ const jsonScriptTypes = new Set([
3593
3629
  'speculationrules',
3594
3630
  ]);
3595
3631
 
3596
- // Tag omission rules and element Sets
3632
+ // Tag omission rules and element sets
3597
3633
 
3598
3634
  // Tag omission rules from https://html.spec.whatwg.org/multipage/syntax.html#optional-tags with the following extensions:
3599
3635
  // - retain `<body>` if followed by `<noscript>`
@@ -3604,35 +3640,35 @@ const optionalStartTags = new Set(['html', 'head', 'body', 'colgroup', 'tbody'])
3604
3640
 
3605
3641
  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
3642
 
3607
- const headerTags = new Set(['meta', 'link', 'script', 'style', 'template', 'noscript']);
3643
+ const headerElements = new Set(['meta', 'link', 'script', 'style', 'template', 'noscript']);
3608
3644
 
3609
- const descriptionTags = new Set(['dt', 'dd']);
3645
+ const descriptionElements = new Set(['dt', 'dd']);
3610
3646
 
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']);
3647
+ 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
3648
 
3613
- const pInlineTags = new Set(['a', 'audio', 'del', 'ins', 'map', 'noscript', 'video']);
3649
+ const pInlineElements = new Set(['a', 'audio', 'del', 'ins', 'map', 'noscript', 'video']);
3614
3650
 
3615
3651
  const rubyEndTagOmission = new Set(['rb', 'rt', 'rtc', 'rp']); // `</rb>`, `</rt>`, `</rp>` can be omitted if followed by `<rb>`, `<rt>`, `<rtc>`, or `<rp>`
3616
3652
 
3617
3653
  const rubyRtcEndTagOmission = new Set(['rb', 'rtc']); // `</rtc>` can be omitted if followed by `<rb>` or `<rtc>` (not `<rt>` or `<rp>`)
3618
3654
 
3619
- const optionTag = new Set(['option', 'optgroup']);
3655
+ const optionElements = new Set(['option', 'optgroup']);
3620
3656
 
3621
- const tableContentTags = new Set(['tbody', 'tfoot']);
3657
+ const tableContentElements = new Set(['tbody', 'tfoot']);
3622
3658
 
3623
- const tableSectionTags = new Set(['thead', 'tbody', 'tfoot']);
3659
+ const tableSectionElements = new Set(['thead', 'tbody', 'tfoot']);
3624
3660
 
3625
- const cellTags = new Set(['td', 'th']);
3661
+ const cellElements = new Set(['td', 'th']);
3626
3662
 
3627
- const topLevelTags = new Set(['html', 'head', 'body']);
3663
+ const topLevelElements = new Set(['html', 'head', 'body']);
3628
3664
 
3629
- const compactTags = new Set(['html', 'body']);
3665
+ const compactElements = new Set(['html', 'body']);
3630
3666
 
3631
- const looseTags = new Set(['head', 'colgroup', 'caption']);
3667
+ const looseElements = new Set(['head', 'colgroup', 'caption']);
3632
3668
 
3633
- const trailingTags = new Set(['dt', 'thead']);
3669
+ const trailingElements = new Set(['dt', 'thead']);
3634
3670
 
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']);
3671
+ 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
3672
 
3637
3673
  // Empty attribute regex
3638
3674
 
@@ -3642,7 +3678,7 @@ const reEmptyAttribute = new RegExp(
3642
3678
 
3643
3679
  // Special content elements
3644
3680
 
3645
- const specialContentTags = new Set(['script', 'style']);
3681
+ const specialContentElements = new Set(['script', 'style']);
3646
3682
 
3647
3683
  // Imports
3648
3684
 
@@ -3706,7 +3742,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
3706
3742
  }
3707
3743
 
3708
3744
  if (trimLeft) {
3709
- // Non-breaking space is specifically handled inside the replacer function
3745
+ // No-break space is specifically handled inside the replacer function
3710
3746
  str = str.replace(/^[ \n\r\t\f\xA0]+/, function (spaces) {
3711
3747
  const conservative = !lineBreakBefore && options.conservativeCollapse;
3712
3748
  if (conservative && spaces === '\t') {
@@ -3717,7 +3753,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
3717
3753
  }
3718
3754
 
3719
3755
  if (trimRight) {
3720
- // Non-breaking space is specifically handled inside the replacer function
3756
+ // No-break space is specifically handled inside the replacer function
3721
3757
  str = str.replace(/[ \n\r\t\f\xA0]+$/, function (spaces) {
3722
3758
  const conservative = !lineBreakAfter && options.conservativeCollapse;
3723
3759
  if (conservative && spaces === '\t') {
@@ -3741,11 +3777,42 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
3741
3777
 
3742
3778
  // Collapse whitespace smartly based on surrounding tags
3743
3779
 
3744
- function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements, inlineTextSet) {
3780
+ function collapseWhitespaceSmart(str, prevTag, nextTag, prevAttrs, nextAttrs, options, inlineElements, inlineTextSet) {
3781
+ const prevTagName = prevTag && (prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag);
3782
+ const nextTagName = nextTag && (nextTag.charAt(0) === '/' ? nextTag.slice(1) : nextTag);
3783
+
3784
+ // Helper: Check if an input element has `type="hidden"`
3785
+ const isHiddenInput = (tagName, attrs) => {
3786
+ if (tagName !== 'input' || !attrs || !attrs.length) return false;
3787
+ const typeAttr = attrs.find(attr => attr.name === 'type');
3788
+ return typeAttr && typeAttr.value === 'hidden';
3789
+ };
3790
+
3791
+ // Check if prev/next are non-rendering (hidden) elements
3792
+ const prevIsHidden = isHiddenInput(prevTagName, prevAttrs);
3793
+ const nextIsHidden = isHiddenInput(nextTagName, nextAttrs);
3794
+
3745
3795
  let trimLeft = prevTag && !inlineElementsToKeepWhitespace.has(prevTag);
3796
+
3797
+ // Smart default behavior: Collapse space after non-rendering elements (`type="hidden"`)
3798
+ // This happens even in basic `collapseWhitespace` mode (safe optimization)
3799
+ if (!trimLeft && prevIsHidden && str && !/\S/.test(str)) {
3800
+ trimLeft = true;
3801
+ }
3802
+
3803
+ // Aggressive mode: Collapse between all form controls (pure whitespace only)
3804
+ const isPureWhitespace = str && !/\S/.test(str);
3805
+ if (!trimLeft && prevTagName && nextTagName &&
3806
+ options.collapseInlineTagWhitespace &&
3807
+ isPureWhitespace &&
3808
+ formControlElements.has(prevTagName) && formControlElements.has(nextTagName)) {
3809
+ trimLeft = true;
3810
+ }
3811
+
3746
3812
  if (trimLeft && !options.collapseInlineTagWhitespace) {
3747
3813
  trimLeft = prevTag.charAt(0) === '/' ? !inlineElements.has(prevTag.slice(1)) : !inlineTextSet.has(prevTag);
3748
3814
  }
3815
+
3749
3816
  // When `collapseInlineTagWhitespace` is enabled, still preserve whitespace around inline text elements
3750
3817
  if (trimLeft && options.collapseInlineTagWhitespace) {
3751
3818
  const tagName = prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag;
@@ -3753,10 +3820,26 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements,
3753
3820
  trimLeft = false;
3754
3821
  }
3755
3822
  }
3823
+
3756
3824
  let trimRight = nextTag && !inlineElementsToKeepWhitespace.has(nextTag);
3825
+
3826
+ // Smart default behavior: Collapse space before non-rendering elements (`type="hidden"`)
3827
+ if (!trimRight && nextIsHidden && str && !/\S/.test(str)) {
3828
+ trimRight = true;
3829
+ }
3830
+
3831
+ // Aggressive mode: Same as `trimLeft`
3832
+ if (!trimRight && prevTagName && nextTagName &&
3833
+ options.collapseInlineTagWhitespace &&
3834
+ isPureWhitespace &&
3835
+ formControlElements.has(prevTagName) && formControlElements.has(nextTagName)) {
3836
+ trimRight = true;
3837
+ }
3838
+
3757
3839
  if (trimRight && !options.collapseInlineTagWhitespace) {
3758
3840
  trimRight = nextTag.charAt(0) === '/' ? !inlineTextSet.has(nextTag.slice(1)) : !inlineElements.has(nextTag);
3759
3841
  }
3842
+
3760
3843
  // When `collapseInlineTagWhitespace` is enabled, still preserve whitespace around inline text elements
3761
3844
  if (trimRight && options.collapseInlineTagWhitespace) {
3762
3845
  const tagName = nextTag.charAt(0) === '/' ? nextTag.slice(1) : nextTag;
@@ -3764,6 +3847,7 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements,
3764
3847
  trimRight = false;
3765
3848
  }
3766
3849
  }
3850
+
3767
3851
  return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
3768
3852
  }
3769
3853
 
@@ -6314,7 +6398,6 @@ var RelateURL = /*@__PURE__*/getDefaultExportFromCjs(libExports);
6314
6398
 
6315
6399
  // Wrap CSS declarations for inline styles and media queries
6316
6400
  // This ensures proper context for CSS minification
6317
-
6318
6401
  function wrapCSS(text, type) {
6319
6402
  switch (type) {
6320
6403
  case 'inline':
@@ -6396,7 +6479,6 @@ async function processScript(text, options, currentAttrs, minifyHTML) {
6396
6479
 
6397
6480
  /**
6398
6481
  * Lightweight SVG optimizations:
6399
- *
6400
6482
  * - Numeric precision reduction for coordinates and path data
6401
6483
  * - Whitespace removal in attribute values (numeric sequences)
6402
6484
  * - Default attribute removal (safe, well-documented defaults)
@@ -6490,7 +6572,7 @@ function minifyNumber(num, precision = 3) {
6490
6572
  if (num === '1.0' || num === '1.00' || num === '1.000') return '1';
6491
6573
 
6492
6574
  // Check cache
6493
- // (Note: uses input string as key, so “0.0000” and “0.00000” create separate entries.
6575
+ // (Note: Uses input string as key, so “0.0000” and “0.00000” create separate entries.
6494
6576
  // This is intentional to avoid parsing overhead.
6495
6577
  // Real-world SVG files from export tools typically use consistent formats.)
6496
6578
  const cacheKey = `${num}:${precision}`;
@@ -6529,15 +6611,15 @@ function minifyPathData(pathData, precision = 3) {
6529
6611
 
6530
6612
  // Remove unnecessary spaces around path commands
6531
6613
  // 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
6614
+ // `M 10 20``M10 20`, `L -5 -3``L-5-3`
6533
6615
  result = result.replace(/([MLHVCSQTAZmlhvcsqtaz])\s+(?=-?\d)/g, '$1');
6534
6616
 
6535
6617
  // Safe to remove space before command letter when preceded by a number
6536
- // 0 L → 0L, 20 M → 20M
6618
+ // `0 L``0L`, `20 M``20M`
6537
6619
  result = result.replace(/(\d)\s+([MLHVCSQTAZmlhvcsqtaz])/g, '$1$2');
6538
6620
 
6539
6621
  // Safe to remove space before negative number when preceded by a number
6540
- // 10 -20 → 10-20 (numbers are separated by the minus sign)
6622
+ // `10 -20``10-20` (numbers are separated by the minus sign)
6541
6623
  result = result.replace(/(\d)\s+(-\d)/g, '$1$2');
6542
6624
 
6543
6625
  return result;
@@ -6546,9 +6628,9 @@ function minifyPathData(pathData, precision = 3) {
6546
6628
  /**
6547
6629
  * Minify whitespace in numeric attribute values
6548
6630
  * Examples:
6549
- * "10 , 20" → "10,20"
6550
- * "translate( 10 20 )" → "translate(10 20)"
6551
- * "100, 10 40, 198" → "100,10 40,198"
6631
+ * - “10 , 20" → "10,20"
6632
+ * - "translate( 10 20 )" → "translate(10 20)"
6633
+ * - "100, 10 40, 198" → "100,10 40,198"
6552
6634
  *
6553
6635
  * @param {string} value - Attribute value to minify
6554
6636
  * @returns {string} Minified value
@@ -6581,8 +6663,7 @@ function minifyColor(color) {
6581
6663
 
6582
6664
  // Don’t process values that aren’t simple colors (preserve case-sensitive references)
6583
6665
  // `url(#id)`, `var(--name)`, `inherit`, `currentColor`, etc.
6584
- if (trimmed.includes('url(') || trimmed.includes('var(') ||
6585
- trimmed === 'inherit' || trimmed === 'currentColor') {
6666
+ if (trimmed.includes('url(') || trimmed.includes('var(') || trimmed === 'inherit' || trimmed === 'currentColor') {
6586
6667
  return trimmed;
6587
6668
  }
6588
6669
 
@@ -6590,7 +6671,7 @@ function minifyColor(color) {
6590
6671
  const lower = trimmed.toLowerCase();
6591
6672
 
6592
6673
  // Shorten 6-digit hex to 3-digit when possible
6593
- // #aabbcc → #abc, #000000 → #000
6674
+ // `#aabbcc``#abc`, `#000000``#000`
6594
6675
  const hexMatch = lower.match(/^#([0-9a-f]{6})$/);
6595
6676
  if (hexMatch) {
6596
6677
  const hex = hexMatch[1];
@@ -6610,7 +6691,7 @@ function minifyColor(color) {
6610
6691
  return NAMED_COLORS[lower] || lower;
6611
6692
  }
6612
6693
 
6613
- // Convert rgb(255,255,255) to hex
6694
+ // Convert rgb() to hex
6614
6695
  const rgbMatch = lower.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
6615
6696
  if (rgbMatch) {
6616
6697
  const r = parseInt(rgbMatch[1], 10);
@@ -6641,14 +6722,14 @@ function minifyColor(color) {
6641
6722
  const NUMERIC_ATTRS = new Set([
6642
6723
  'd', // Path data
6643
6724
  'points', // Polygon/polyline points
6644
- 'viewBox', // viewBox coordinates
6725
+ 'viewBox', // `viewBox` coordinates
6645
6726
  'transform', // Transform functions
6646
6727
  'x', 'y', 'x1', 'y1', 'x2', 'y2', // Coordinates
6647
6728
  'cx', 'cy', 'r', 'rx', 'ry', // Circle/ellipse
6648
6729
  'width', 'height', // Dimensions
6649
6730
  'dx', 'dy', // Text offsets
6650
6731
  'offset', // Gradient offset
6651
- 'startOffset', // textPath
6732
+ 'startOffset', // `textPath`
6652
6733
  'pathLength', // Path length
6653
6734
  'stdDeviation', // Filter params
6654
6735
  'baseFrequency', // Turbulence
@@ -6832,8 +6913,8 @@ function shouldMinifyInnerHTML(options) {
6832
6913
  /**
6833
6914
  * @param {Partial<MinifierOptions>} inputOptions - User-provided options
6834
6915
  * @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
6916
+ * @param {Function} deps.getLightningCSS - Function to lazily load Lightning CSS
6917
+ * @param {Function} deps.getTerser - Function to lazily load Terser
6837
6918
  * @param {Function} deps.getSwc - Function to lazily load @swc/core
6838
6919
  * @param {LRU} deps.cssMinifyCache - CSS minification cache
6839
6920
  * @param {LRU} deps.jsMinifyCache - JS minification cache
@@ -6870,7 +6951,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
6870
6951
  if (typeof value === 'string') {
6871
6952
  return new RegExp(value.replace(/^\/(.*)\/$/, '$1'));
6872
6953
  }
6873
- return value; // Already a RegExp or other type
6954
+ return value; // Already a RegExp or another type
6874
6955
  };
6875
6956
 
6876
6957
  const parseRegExpArray = (arr) => {
@@ -6890,9 +6971,25 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
6890
6971
  });
6891
6972
  };
6892
6973
 
6974
+ // Apply preset first if specified (so user options can override preset values)
6975
+ if (inputOptions.preset) {
6976
+ const preset = getPreset(inputOptions.preset);
6977
+ if (preset) {
6978
+ Object.assign(options, preset);
6979
+ } else {
6980
+ const available = getPresetNames().join(', ');
6981
+ console.warn(`HTML Minifier Next: Unknown preset “${inputOptions.preset}”. Available presets: ${available}`);
6982
+ }
6983
+ }
6984
+
6893
6985
  Object.keys(inputOptions).forEach(function (key) {
6894
6986
  const option = inputOptions[key];
6895
6987
 
6988
+ // Skip preset key—it’s already been processed
6989
+ if (key === 'preset') {
6990
+ return;
6991
+ }
6992
+
6896
6993
  if (key === 'caseSensitive') {
6897
6994
  if (option) {
6898
6995
  options.name = identity;
@@ -7007,7 +7104,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
7007
7104
  // Validate engine
7008
7105
  const supportedEngines = ['terser', 'swc'];
7009
7106
  if (!supportedEngines.includes(engine)) {
7010
- throw new Error(`Unsupported JS minifier engine: "${engine}". Supported engines: ${supportedEngines.join(', ')}`);
7107
+ throw new Error(`Unsupported JS minifier engine: “${engine}”. Supported engines: ${supportedEngines.join(', ')}`);
7011
7108
  }
7012
7109
 
7013
7110
  // Extract engine-specific options (excluding `engine` field itself)
@@ -7114,14 +7211,14 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
7114
7211
  relateUrlOptions = {};
7115
7212
  }
7116
7213
 
7117
- // Cache RelateURL instance for reuse (expensive to create)
7214
+ // Cache relateurl instance for reuse (expensive to create)
7118
7215
  const relateUrlInstance = new RelateURL(relateUrlOptions.site || '', relateUrlOptions);
7119
7216
 
7120
7217
  // Create instance-specific cache (results depend on site configuration)
7121
7218
  const instanceCache = urlMinifyCache ? new (urlMinifyCache.constructor)(500) : null;
7122
7219
 
7123
7220
  options.minifyURLs = function (text) {
7124
- // Fast-path: Skip if text doesn't look like a URL that needs processing
7221
+ // Fast-path: Skip if text doesnt look like a URL that needs processing
7125
7222
  // Only process if contains URL-like characters (`/`, `:`, `#`, `?`) or spaces that need encoding
7126
7223
  if (!/[/:?#\s]/.test(text)) {
7127
7224
  return text;
@@ -7153,17 +7250,17 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
7153
7250
  };
7154
7251
  } else if (key === 'minifySVG') {
7155
7252
  // Process SVG minification options
7156
- // Unlike minifyCSS/minifyJS, this is a simple options object, not a function
7253
+ // Unlike `minifyCSS`/`minifyJS`, this is a simple options object, not a function
7157
7254
  // The actual minification is applied inline during attribute processing
7158
7255
  options.minifySVG = getSVGMinifierOptions(option);
7159
7256
  } else if (key === 'customAttrCollapse') {
7160
- // Single RegExp pattern
7257
+ // Single regex pattern
7161
7258
  options[key] = parseRegExp(option);
7162
7259
  } else if (key === 'customAttrSurround') {
7163
7260
  // Nested array of RegExp pairs: `[[openRegExp, closeRegExp], …]`
7164
7261
  options[key] = parseNestedRegExpArray(option);
7165
7262
  } else if (['customAttrAssign', 'customEventAttributes', 'ignoreCustomComments', 'ignoreCustomFragments'].includes(key)) {
7166
- // Array of RegExp patterns
7263
+ // Array of regex patterns
7167
7264
  options[key] = parseRegExpArray(option);
7168
7265
  } else {
7169
7266
  options[key] = option;
@@ -7228,8 +7325,7 @@ function isAttributeRedundant(tag, attrName, attrValue, attrs) {
7228
7325
  const tagHasDefaults = tag in tagDefaults;
7229
7326
 
7230
7327
  // Check for legacy attribute rules (element- and attribute-specific)
7231
- const isLegacyAttr = (tag === 'script' && (attrName === 'language' || attrName === 'charset')) ||
7232
- (tag === 'a' && attrName === 'name');
7328
+ const isLegacyAttr = (tag === 'script' && (attrName === 'language' || attrName === 'charset')) || (tag === 'a' && attrName === 'name');
7233
7329
 
7234
7330
  // If none of these conditions apply, attribute cannot be redundant
7235
7331
  if (!hasGeneralDefault && !tagHasDefaults && !isLegacyAttr) {
@@ -7287,7 +7383,7 @@ function isStyleLinkTypeAttribute(attrValue = '') {
7287
7383
  return attrValue === '' || attrValue === 'text/css';
7288
7384
  }
7289
7385
 
7290
- function isStyleSheet(tag, attrs) {
7386
+ function isStyleElement(tag, attrs) {
7291
7387
  if (tag !== 'style') {
7292
7388
  return false;
7293
7389
  }
@@ -7344,11 +7440,11 @@ function isLinkType(tag, attrs, value) {
7344
7440
  }
7345
7441
 
7346
7442
  function isMediaQuery(tag, attrs, attrName) {
7347
- return attrName === 'media' && (isLinkType(tag, attrs, 'stylesheet') || isStyleSheet(tag, attrs));
7443
+ return attrName === 'media' && (isLinkType(tag, attrs, 'stylesheet') || isStyleElement(tag, attrs));
7348
7444
  }
7349
7445
 
7350
7446
  function isSrcset(attrName, tag) {
7351
- return attrName === 'srcset' && srcsetTags.has(tag);
7447
+ return attrName === 'srcset' && srcsetElements.has(tag);
7352
7448
  }
7353
7449
 
7354
7450
  function isMetaViewport(tag, attrs) {
@@ -7356,7 +7452,7 @@ function isMetaViewport(tag, attrs) {
7356
7452
  return false;
7357
7453
  }
7358
7454
  for (let i = 0, len = attrs.length; i < len; i++) {
7359
- if (attrs[i].name === 'name' && attrs[i].value === 'viewport') {
7455
+ if (attrs[i].name.toLowerCase() === 'name' && attrs[i].value.toLowerCase() === 'viewport') {
7360
7456
  return true;
7361
7457
  }
7362
7458
  }
@@ -7376,7 +7472,7 @@ function isContentSecurityPolicy(tag, attrs) {
7376
7472
  }
7377
7473
 
7378
7474
  function canDeleteEmptyAttribute(tag, attrName, attrValue, options) {
7379
- const isValueEmpty = !attrValue || /^\s*$/.test(attrValue);
7475
+ const isValueEmpty = !attrValue || attrValue.trim() === '';
7380
7476
  if (!isValueEmpty) {
7381
7477
  return false;
7382
7478
  }
@@ -7399,7 +7495,7 @@ function hasAttrName(name, attrs) {
7399
7495
 
7400
7496
  async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTMLSelf) {
7401
7497
  // Apply early whitespace normalization if enabled
7402
- // Preserves special spaces (non-breaking space, hair space, etc.) for consistency with `collapseWhitespace`
7498
+ // Preserves special spaces (no-break space, hair space, etc.) for consistency with `collapseWhitespace`
7403
7499
  if (options.collapseAttributeWhitespace) {
7404
7500
  // Fast path: Only process if whitespace exists (avoids regex overhead on clean values)
7405
7501
  if (RE_ATTR_WS_CHECK.test(attrValue)) {
@@ -7455,7 +7551,7 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
7455
7551
  try {
7456
7552
  attrValue = await options.minifyCSS(attrValue, 'inline');
7457
7553
  // After minification, check if CSS consists entirely of invalid properties (no values)
7458
- // E.g., `color:` or `margin:;padding:` should be treated as empty
7554
+ // I.e., `color:` or `margin:;padding:` should be treated as empty
7459
7555
  if (attrValue && /^(?:[a-z-]+:\s*;?\s*)+$/i.test(attrValue)) {
7460
7556
  attrValue = '';
7461
7557
  }
@@ -7575,13 +7671,13 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
7575
7671
  }
7576
7672
 
7577
7673
  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))) {
7674
+ isAttributeRedundant(tag, attrName, attrValue, attrs)) ||
7675
+ (options.removeScriptTypeAttributes && tag === 'script' &&
7676
+ attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue)) ||
7677
+ (options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
7678
+ attrName === 'type' && isStyleLinkTypeAttribute(attrValue)) ||
7679
+ (options.insideSVG && options.minifySVG &&
7680
+ shouldRemoveSVGAttribute(tag, attrName, attrValue, options.minifySVG))) {
7585
7681
  return;
7586
7682
  }
7587
7683
 
@@ -7590,7 +7686,7 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
7590
7686
  }
7591
7687
 
7592
7688
  if (options.removeEmptyAttributes &&
7593
- canDeleteEmptyAttribute(tag, attrName, attrValue, options)) {
7689
+ canDeleteEmptyAttribute(tag, attrName, attrValue, options)) {
7594
7690
  return;
7595
7691
  }
7596
7692
 
@@ -7613,19 +7709,35 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
7613
7709
  let attrFragment;
7614
7710
  let emittedAttrValue;
7615
7711
 
7616
- if (typeof attrValue !== 'undefined' && (!options.removeAttributeQuotes ||
7617
- attrValue.indexOf(uidAttr) !== -1 || !canRemoveAttributeQuotes(attrValue))) {
7712
+ // Determine if we need to add/keep quotes
7713
+ const shouldAddQuotes = typeof attrValue !== 'undefined' && (
7714
+ // If `removeAttributeQuotes` is enabled, add quotes only if they can’t be removed
7715
+ (options.removeAttributeQuotes && (attrValue.indexOf(uidAttr) !== -1 || !canRemoveAttributeQuotes(attrValue))) ||
7716
+ // If `removeAttributeQuotes` is not enabled, preserve original quote style or add quotes if value requires them
7717
+ (!options.removeAttributeQuotes && (attrQuote !== '' || !canRemoveAttributeQuotes(attrValue) ||
7718
+ // Special case: With `removeTagWhitespace`, unquoted values that aren’t last will have space added,
7719
+ // which can create ambiguous/invalid HTML—add quotes to be safe
7720
+ (options.removeTagWhitespace && attrQuote === '' && !isLast)))
7721
+ );
7722
+
7723
+ if (shouldAddQuotes) {
7618
7724
  // Determine the appropriate quote character
7619
7725
  if (!options.preventAttributesEscaping) {
7620
- // Normal mode: choose quotes and escape
7621
- attrQuote = chooseAttributeQuote(attrValue, options);
7726
+ // Normal mode: Choose optimal quote type to minimize escaping
7727
+ // unless we’re preserving original quotes and they don’t need escaping
7728
+ const needsEscaping = (attrQuote === '"' && attrValue.indexOf('"') !== -1) || (attrQuote === "'" && attrValue.indexOf("'") !== -1);
7729
+
7730
+ if (options.removeAttributeQuotes || typeof options.quoteCharacter !== 'undefined' || needsEscaping || attrQuote === '') {
7731
+ attrQuote = chooseAttributeQuote(attrValue, options);
7732
+ }
7733
+
7622
7734
  if (attrQuote === '"') {
7623
7735
  attrValue = attrValue.replace(/"/g, '&#34;');
7624
7736
  } else {
7625
7737
  attrValue = attrValue.replace(/'/g, '&#39;');
7626
7738
  }
7627
7739
  } else {
7628
- // `preventAttributesEscaping` mode: choose safe quotes but don't escape
7740
+ // `preventAttributesEscaping` mode: Choose safe quotes but don't escape
7629
7741
  // except when both quote types are present—then escape to prevent invalid HTML
7630
7742
  const hasDoubleQuote = attrValue.indexOf('"') !== -1;
7631
7743
  const hasSingleQuote = attrValue.indexOf("'") !== -1;
@@ -7644,8 +7756,18 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
7644
7756
  attrQuote = "'";
7645
7757
  } else if (attrQuote === "'" && hasSingleQuote && !hasDoubleQuote) {
7646
7758
  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 !== '') {
7759
+ // If no quote character yet (empty string), choose based on content
7760
+ } else if (attrQuote === '') {
7761
+ if (hasSingleQuote && !hasDoubleQuote) {
7762
+ attrQuote = '"';
7763
+ } else if (hasDoubleQuote && !hasSingleQuote) {
7764
+ attrQuote = "'";
7765
+ } else {
7766
+ attrQuote = '"';
7767
+ }
7768
+ // Fallback for invalid/unsupported attrQuote values (not `"`, `'`, or empty string):
7769
+ // Choose safe default based on value content
7770
+ } else if (attrQuote !== '"' && attrQuote !== "'") {
7649
7771
  if (hasSingleQuote && !hasDoubleQuote) {
7650
7772
  attrQuote = '"';
7651
7773
  } else if (hasDoubleQuote && !hasSingleQuote) {
@@ -7655,7 +7777,22 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
7655
7777
  }
7656
7778
  }
7657
7779
  } else {
7658
- attrQuote = options.quoteCharacter === '\'' ? '\'' : '"';
7780
+ // `quoteCharacter` is explicitly set
7781
+ const preferredQuote = options.quoteCharacter === '\'' ? '\'' : '"';
7782
+ // Safety check: If the preferred quote conflicts with value content, switch to the opposite quote
7783
+ if ((preferredQuote === '"' && hasDoubleQuote && !hasSingleQuote) || (preferredQuote === "'" && hasSingleQuote && !hasDoubleQuote)) {
7784
+ attrQuote = preferredQuote === '"' ? "'" : '"';
7785
+ } else if ((preferredQuote === '"' && hasDoubleQuote && hasSingleQuote) || (preferredQuote === "'" && hasSingleQuote && hasDoubleQuote)) {
7786
+ // Both quote types present: Fall back to escaping despite `preventAttributesEscaping`
7787
+ attrQuote = preferredQuote;
7788
+ if (attrQuote === '"') {
7789
+ attrValue = attrValue.replace(/"/g, '&#34;');
7790
+ } else {
7791
+ attrValue = attrValue.replace(/'/g, '&#39;');
7792
+ }
7793
+ } else {
7794
+ attrQuote = preferredQuote;
7795
+ }
7659
7796
  }
7660
7797
  }
7661
7798
  emittedAttrValue = attrQuote + attrValue + attrQuote;
@@ -7663,15 +7800,17 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
7663
7800
  emittedAttrValue += ' ';
7664
7801
  }
7665
7802
  } else if (isLast && !hasUnarySlash) {
7666
- // Last attribute in a non-self-closing tag: no space needed
7803
+ // Last attribute in a non-self-closing tag:
7804
+ // No space needed
7667
7805
  emittedAttrValue = attrValue;
7668
7806
  } else {
7669
- // Not last attribute, or is a self-closing tag: add space
7807
+ // Not last attribute, or is a self-closing tag:
7808
+ // Unquoted values must have space after them to delimit from next attribute
7670
7809
  emittedAttrValue = attrValue + ' ';
7671
7810
  }
7672
7811
 
7673
7812
  if (typeof attrValue === 'undefined' || (options.collapseBooleanAttributes &&
7674
- isBooleanAttribute(attrName.toLowerCase(), (attrValue || '').toLowerCase()))) {
7813
+ isBooleanAttribute(attrName.toLowerCase(), (attrValue || '').toLowerCase()))) {
7675
7814
  attrFragment = attrName;
7676
7815
  if (!isLast) {
7677
7816
  attrFragment += ' ';
@@ -7694,7 +7833,7 @@ function canRemoveParentTag(optionalStartTag, tag) {
7694
7833
  case 'head':
7695
7834
  return true;
7696
7835
  case 'body':
7697
- return !headerTags.has(tag);
7836
+ return !headerElements.has(tag);
7698
7837
  case 'colgroup':
7699
7838
  return tag === 'col';
7700
7839
  case 'tbody':
@@ -7708,7 +7847,7 @@ function isStartTagMandatory(optionalEndTag, tag) {
7708
7847
  case 'colgroup':
7709
7848
  return optionalEndTag === 'colgroup';
7710
7849
  case 'tbody':
7711
- return tableSectionTags.has(optionalEndTag);
7850
+ return tableSectionElements.has(optionalEndTag);
7712
7851
  }
7713
7852
  return false;
7714
7853
  }
@@ -7727,9 +7866,9 @@ function canRemovePrecedingTag(optionalEndTag, tag) {
7727
7866
  return tag === optionalEndTag;
7728
7867
  case 'dt':
7729
7868
  case 'dd':
7730
- return descriptionTags.has(tag);
7869
+ return descriptionElements.has(tag);
7731
7870
  case 'p':
7732
- return pBlockTags.has(tag);
7871
+ return pBlockElements.has(tag);
7733
7872
  case 'rb':
7734
7873
  case 'rt':
7735
7874
  case 'rp':
@@ -7737,15 +7876,15 @@ function canRemovePrecedingTag(optionalEndTag, tag) {
7737
7876
  case 'rtc':
7738
7877
  return rubyRtcEndTagOmission.has(tag);
7739
7878
  case 'option':
7740
- return optionTag.has(tag);
7879
+ return optionElements.has(tag);
7741
7880
  case 'thead':
7742
7881
  case 'tbody':
7743
- return tableContentTags.has(tag);
7882
+ return tableContentElements.has(tag);
7744
7883
  case 'tfoot':
7745
7884
  return tag === 'tbody';
7746
7885
  case 'td':
7747
7886
  case 'th':
7748
- return cellTags.has(tag);
7887
+ return cellElements.has(tag);
7749
7888
  }
7750
7889
  return false;
7751
7890
  }
@@ -7848,7 +7987,7 @@ function parseRemoveEmptyElementsExcept(input, options) {
7848
7987
  if (typeof item === 'string') {
7849
7988
  const spec = parseElementSpec(item, options);
7850
7989
  if (!spec && options.log) {
7851
- options.log('Warning: Unable to parse “removeEmptyElementsExcept” specification: "' + item + '"');
7990
+ options.log('Warning: Unable to parse “removeEmptyElementsExcept” specification: ' + item + '');
7852
7991
  }
7853
7992
  return spec;
7854
7993
  }
@@ -8361,7 +8500,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
8361
8500
  }
8362
8501
 
8363
8502
  // Pre-compile regex patterns for reuse (performance optimization)
8364
- // These must be declared before scan() since scan uses them
8503
+ // These must be declared before `scan()` since scan uses them
8365
8504
  const whitespaceSplitPatternScan = /[ \t\n\f\r]+/;
8366
8505
  const whitespaceSplitPatternSort = /[ \n\f\r]+/;
8367
8506
 
@@ -8393,9 +8532,9 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
8393
8532
  chars: async function (text) {
8394
8533
  // Only recursively scan HTML content, not JSON-LD or other non-HTML script types
8395
8534
  // `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') {
8535
+ if (options.processScripts && specialContentElements.has(currentTag) &&
8536
+ options.processScripts.indexOf(currentType) > -1 &&
8537
+ currentType === 'text/html') {
8399
8538
  await scan(text);
8400
8539
  }
8401
8540
  },
@@ -8418,7 +8557,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
8418
8557
  // For the first pass, create a copy of options and disable aggressive minification.
8419
8558
  // Keep attribute transformations (like `removeStyleLinkTypeAttributes`) for accurate analysis.
8420
8559
  // 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.
8560
+ // Note: `htmlmin:ignore` UID markers (`uidIgnore`) already exist and are expanded for analysis.
8422
8561
  const firstPassOptions = Object.assign({}, options, {
8423
8562
  // Disable sorting for the analysis pass
8424
8563
  sortAttributes: false,
@@ -8437,7 +8576,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
8437
8576
  });
8438
8577
 
8439
8578
  // Temporarily enable `continueOnParseError` for the `scan()` function call below.
8440
- // Note: `firstPassOptions` already has `continueOnParseError: true` for the minifyHTML call.
8579
+ // Note: `firstPassOptions` already has `continueOnParseError: true` for the `minifyHTML` call.
8441
8580
  const originalContinueOnParseError = options.continueOnParseError;
8442
8581
  options.continueOnParseError = true;
8443
8582
 
@@ -8450,7 +8589,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
8450
8589
  : null;
8451
8590
 
8452
8591
  try {
8453
- // Expand UID tokens back to original content for frequency analysis
8592
+ // Expand UID tokens back to the original content for frequency analysis
8454
8593
  let expandedValue = value;
8455
8594
  if (uidReplacePattern) {
8456
8595
  expandedValue = value.replace(uidReplacePattern, function (match, index) {
@@ -8499,7 +8638,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
8499
8638
  attrOrderCache.set(cacheKey, sortedNames);
8500
8639
  }
8501
8640
 
8502
- // Apply the sorted order to attrs
8641
+ // Apply the sorted order to `attrs`
8503
8642
  const attrMap = Object.create(null);
8504
8643
  names.forEach(function (name, index) {
8505
8644
  (attrMap[name] || (attrMap[name] = [])).push(attrs[index]);
@@ -8586,7 +8725,7 @@ async function minifyHTML(value, options, partialMarkup) {
8586
8725
  const customElementsInput = options.inlineCustomElements ?? [];
8587
8726
  const customElementsArr = Array.isArray(customElementsInput) ? customElementsInput : Array.from(customElementsInput);
8588
8727
  const normalizedCustomElements = customElementsArr.map(name => options.name(name));
8589
- // Fast path: Reuse base Sets if no custom elements
8728
+ // Fast path: Reuse base sets if no custom elements
8590
8729
  const inlineTextSet = normalizedCustomElements.length
8591
8730
  ? new Set([...inlineElementsToKeepWhitespaceWithin, ...normalizedCustomElements])
8592
8731
  : inlineElementsToKeepWhitespaceWithin;
@@ -8606,7 +8745,7 @@ async function minifyHTML(value, options, partialMarkup) {
8606
8745
  }
8607
8746
 
8608
8747
  // 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
8748
+ // For all we care there might be completely-horribly-broken-alien-non-html-emoji-cthulhu-filled content
8610
8749
  value = value.replace(/<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g, function (match, group1) {
8611
8750
  if (!uidIgnore) {
8612
8751
  uidIgnore = uniqueId(value);
@@ -8627,7 +8766,7 @@ async function minifyHTML(value, options, partialMarkup) {
8627
8766
  // Create sort functions after `htmlmin:ignore` processing but before custom fragment UID markers
8628
8767
  // This allows proper frequency analysis with access to ignored content via UID tokens
8629
8768
  if ((options.sortAttributes && typeof options.sortAttributes !== 'function') ||
8630
- (options.sortClassName && typeof options.sortClassName !== 'function')) {
8769
+ (options.sortClassName && typeof options.sortClassName !== 'function')) {
8631
8770
  await createSortFns(value, options, uidIgnore, null, ignoredMarkupChunks);
8632
8771
  }
8633
8772
 
@@ -8689,11 +8828,11 @@ async function minifyHTML(value, options, partialMarkup) {
8689
8828
  });
8690
8829
  }
8691
8830
 
8692
- function _canCollapseWhitespace(tag, attrs) {
8831
+ function canCollapseWhitespace$1(tag, attrs) {
8693
8832
  return options.canCollapseWhitespace(tag, attrs, canCollapseWhitespace);
8694
8833
  }
8695
8834
 
8696
- function _canTrimWhitespace(tag, attrs) {
8835
+ function canTrimWhitespace$1(tag, attrs) {
8697
8836
  return options.canTrimWhitespace(tag, attrs, canTrimWhitespace);
8698
8837
  }
8699
8838
 
@@ -8715,12 +8854,12 @@ async function minifyHTML(value, options, partialMarkup) {
8715
8854
 
8716
8855
  // Look for trailing whitespaces, bypass any inline tags
8717
8856
  function trimTrailingWhitespace(index, nextTag) {
8718
- for (let endTag = null; index >= 0 && _canTrimWhitespace(endTag); index--) {
8857
+ for (let endTag = null; index >= 0 && canTrimWhitespace$1(endTag); index--) {
8719
8858
  const str = buffer[index];
8720
8859
  const match = str.match(/^<\/([\w:-]+)>$/);
8721
8860
  if (match) {
8722
8861
  endTag = match[1];
8723
- } else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, options, inlineElements, inlineTextSet))) {
8862
+ } else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, [], [], options, inlineElements, inlineTextSet))) {
8724
8863
  break;
8725
8864
  }
8726
8865
  }
@@ -8769,10 +8908,10 @@ async function minifyHTML(value, options, partialMarkup) {
8769
8908
 
8770
8909
  let optional = options.removeOptionalTags;
8771
8910
  if (optional) {
8772
- const htmlTag = htmlTags.has(tag);
8911
+ const htmlTag = htmlElements.has(tag);
8773
8912
  // `<html>` may be omitted if first thing inside is not a comment
8774
8913
  // `<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>`
8914
+ // `<body>` may be omitted if first thing inside is not space, comment, `<meta>`, `<link>`, `<script>`, `<style>`, or `<template>`
8776
8915
  // `<colgroup>` may be omitted if first thing inside is `<col>`
8777
8916
  // `<tbody>` may be omitted if first thing inside is `<tr>`
8778
8917
  if (htmlTag && canRemoveParentTag(optionalStartTag, tag)) {
@@ -8789,16 +8928,16 @@ async function minifyHTML(value, options, partialMarkup) {
8789
8928
  optionalEndTag = '';
8790
8929
  }
8791
8930
 
8792
- // Set whitespace flags for nested tags (e.g., <code> within a <pre>)
8931
+ // Set whitespace flags for nested tags (e.g., `<code>` within a `<pre>`)
8793
8932
  if (options.collapseWhitespace) {
8794
8933
  if (!stackNoTrimWhitespace.length) {
8795
8934
  squashTrailingWhitespace(tag);
8796
8935
  }
8797
8936
  if (!unary) {
8798
- if (!_canTrimWhitespace(tag, attrs) || stackNoTrimWhitespace.length) {
8937
+ if (!canTrimWhitespace$1(tag, attrs) || stackNoTrimWhitespace.length) {
8799
8938
  stackNoTrimWhitespace.push(tag);
8800
8939
  }
8801
- if (!_canCollapseWhitespace(tag, attrs) || stackNoCollapseWhitespace.length) {
8940
+ if (!canCollapseWhitespace$1(tag, attrs) || stackNoCollapseWhitespace.length) {
8802
8941
  stackNoCollapseWhitespace.push(tag);
8803
8942
  }
8804
8943
  }
@@ -8854,7 +8993,7 @@ async function minifyHTML(value, options, partialMarkup) {
8854
8993
  squashTrailingWhitespace('/' + tag);
8855
8994
  }
8856
8995
  if (stackNoCollapseWhitespace.length &&
8857
- tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
8996
+ tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
8858
8997
  stackNoCollapseWhitespace.pop();
8859
8998
  }
8860
8999
  }
@@ -8867,7 +9006,7 @@ async function minifyHTML(value, options, partialMarkup) {
8867
9006
 
8868
9007
  if (options.removeOptionalTags) {
8869
9008
  // `<html>`, `<head>` or `<body>` may be omitted if the element is empty
8870
- if (isElementEmpty && topLevelTags.has(optionalStartTag)) {
9009
+ if (isElementEmpty && topLevelElements.has(optionalStartTag)) {
8871
9010
  removeStartTag();
8872
9011
  }
8873
9012
  optionalStartTag = '';
@@ -8875,7 +9014,7 @@ async function minifyHTML(value, options, partialMarkup) {
8875
9014
  // `</head>` may be omitted if not followed by space or comment
8876
9015
  // `</p>` may be omitted if no more content in non-`</a>` parent
8877
9016
  // 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))) {
9017
+ if (tag && optionalEndTag && !trailingElements.has(optionalEndTag) && (optionalEndTag !== 'p' || !pInlineElements.has(tag))) {
8879
9018
  removeEndTag();
8880
9019
  }
8881
9020
  optionalEndTag = optionalEndTags.has(tag) ? tag : '';
@@ -8922,10 +9061,12 @@ async function minifyHTML(value, options, partialMarkup) {
8922
9061
  }
8923
9062
  }
8924
9063
  },
8925
- chars: async function (text, prevTag, nextTag) {
9064
+ chars: async function (text, prevTag, nextTag, prevAttrs, nextAttrs) {
8926
9065
  prevTag = prevTag === '' ? 'comment' : prevTag;
8927
9066
  nextTag = nextTag === '' ? 'comment' : nextTag;
8928
- if (options.decodeEntities && text && !specialContentTags.has(currentTag)) {
9067
+ prevAttrs = prevAttrs || [];
9068
+ nextAttrs = nextAttrs || [];
9069
+ if (options.decodeEntities && text && !specialContentElements.has(currentTag)) {
8929
9070
  if (text.indexOf('&') !== -1) {
8930
9071
  text = decodeHTML(text);
8931
9072
  }
@@ -8961,7 +9102,7 @@ async function minifyHTML(value, options, partialMarkup) {
8961
9102
  }
8962
9103
  }
8963
9104
  if (prevTag || nextTag) {
8964
- text = collapseWhitespaceSmart(text, prevTag, nextTag, options, inlineElements, inlineTextSet);
9105
+ text = collapseWhitespaceSmart(text, prevTag, nextTag, prevAttrs, nextAttrs, options, inlineElements, inlineTextSet);
8965
9106
  } else {
8966
9107
  text = collapseWhitespace(text, options, true, true);
8967
9108
  }
@@ -8973,13 +9114,13 @@ async function minifyHTML(value, options, partialMarkup) {
8973
9114
  text = collapseWhitespace(text, options, false, false, true);
8974
9115
  }
8975
9116
  }
8976
- if (specialContentTags.has(currentTag) && (options.processScripts || hasJsonScriptType(currentAttrs))) {
9117
+ if (specialContentElements.has(currentTag) && (options.processScripts || hasJsonScriptType(currentAttrs))) {
8977
9118
  text = await processScript(text, options, currentAttrs, minifyHTML);
8978
9119
  }
8979
9120
  if (isExecutableScript(currentTag, currentAttrs)) {
8980
9121
  text = await options.minifyJS(text);
8981
9122
  }
8982
- if (isStyleSheet(currentTag, currentAttrs)) {
9123
+ if (isStyleElement(currentTag, currentAttrs)) {
8983
9124
  text = await options.minifyCSS(text);
8984
9125
  }
8985
9126
  if (options.removeOptionalTags && text) {
@@ -8991,7 +9132,7 @@ async function minifyHTML(value, options, partialMarkup) {
8991
9132
  optionalStartTag = '';
8992
9133
  // `</html>` or `</body>` may be omitted if not followed by comment
8993
9134
  // `</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))) {
9135
+ if (compactElements.has(optionalEndTag) || (looseElements.has(optionalEndTag) && !/^\s/.test(text))) {
8995
9136
  removeEndTag();
8996
9137
  }
8997
9138
  // Don’t reset optionalEndTag if text is only whitespace and will be collapsed (not conservatively)
@@ -9000,11 +9141,11 @@ async function minifyHTML(value, options, partialMarkup) {
9000
9141
  }
9001
9142
  }
9002
9143
  charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
9003
- if (options.decodeEntities && text && !specialContentTags.has(currentTag)) {
9144
+ if (options.decodeEntities && text && !specialContentElements.has(currentTag)) {
9004
9145
  // Escape any `&` symbols that start either:
9005
- // 1) a legacy named character reference (i.e., one that doesn’t end with `;`)
9146
+ // 1) a legacy-named character reference (i.e., one that doesn’t end with `;`)
9006
9147
  // 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.
9148
+ // Note that `&` can be escaped as `&amp`, without the semicolon.
9008
9149
  // https://mathiasbynens.be/notes/ambiguous-ampersands
9009
9150
  if (text.indexOf('&') !== -1) {
9010
9151
  text = text.replace(RE_LEGACY_ENTITIES, '&amp$1');
@@ -9069,7 +9210,7 @@ async function minifyHTML(value, options, partialMarkup) {
9069
9210
 
9070
9211
  // Only collapse whitespace if both blocks contain HTML (start with `<`)
9071
9212
  // 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
9213
+ // Note: This check will match HTML comments (`<!-- … -->`), but the tag name
9073
9214
  // regex below requires starting with a letter, so comments are intentionally
9074
9215
  // excluded by the `currentTagMatch && prevTagMatch` guard
9075
9216
  if (currentContent && prevContent && /^\s*</.test(currentContent) && /^\s*</.test(prevContent)) {
@@ -9130,11 +9271,11 @@ async function minifyHTML(value, options, partialMarkup) {
9130
9271
  if (options.removeOptionalTags) {
9131
9272
  // `<html>` may be omitted if first thing inside is not a comment
9132
9273
  // `<head>` or `<body>` may be omitted if empty
9133
- if (topLevelTags.has(optionalStartTag)) {
9274
+ if (topLevelElements.has(optionalStartTag)) {
9134
9275
  removeStartTag();
9135
9276
  }
9136
9277
  // except for `</dt>` or `</thead>`, end tags may be omitted if no more content in parent element
9137
- if (optionalEndTag && !trailingTags.has(optionalEndTag)) {
9278
+ if (optionalEndTag && !trailingElements.has(optionalEndTag)) {
9138
9279
  removeEndTag();
9139
9280
  }
9140
9281
  }