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