html-minifier-next 4.9.0 → 4.9.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.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  HTML Minifier Next (HMN) is a **super-configurable, well-tested, JavaScript-based HTML minifier**.
6
6
 
7
- The project was based on [HTML Minifier Terser](https://github.com/terser/html-minifier-terser), which in turn had been based on [Juriy Zaytsev’s HTML Minifier](https://github.com/kangax/html-minifier). HMN offers additional features, but is backwards-compatible with both. The project was set up because as of 2025, both HTML Minifier Terser and HTML Minifier had been unmaintained for a few years. As the project seems maintainable [to me, [Jens](https://meiert.com/)]—even more so with community support—, it’s being [updated, extended, and documented](https://github.com/j9t/html-minifier-next/blob/main/CHANGELOG.md) further in this place.
7
+ The project was based on [HTML Minifier Terser](https://github.com/terser/html-minifier-terser), which in turn had been based on [Juriy “kangax” Zaytsev’s HTML Minifier](https://github.com/kangax/html-minifier). HMN offers additional features, but is backwards-compatible with both. The project was set up because as of 2025, both HTML Minifier Terser and HTML Minifier had been unmaintained for a few years. As the project seems maintainable [to me, [Jens](https://meiert.com/), an HTML optimizer]—even more so with community support—, it’s being [updated, extended, and documented](https://github.com/j9t/html-minifier-next/blob/main/CHANGELOG.md) further in this place.
8
8
 
9
9
  ## Installation
10
10
 
@@ -431,4 +431,4 @@ npm run benchmarks
431
431
 
432
432
  ## Acknowledgements
433
433
 
434
- With many thanks to all the previous authors of HTML Minifier, especially [Juriy Zaytsev](https://github.com/kangax), and to everyone who helped make this new edition better, particularly [Daniel Ruf](https://github.com/DanielRuf) and [Jonas Geiler](https://github.com/jonasgeiler).
434
+ With many thanks to all the previous authors of HTML Minifier, especially [Juriy “kangax” Zaytsev](https://github.com/kangax), and to everyone who helped make this new edition better, particularly [Daniel Ruf](https://github.com/DanielRuf) and [Jonas Geiler](https://github.com/jonasgeiler).
@@ -2363,6 +2363,7 @@ async function minifyHTML(value, options, partialMarkup) {
2363
2363
  const ignoredMarkupChunks = [];
2364
2364
  const ignoredCustomMarkupChunks = [];
2365
2365
  let uidIgnore;
2366
+ let uidIgnorePlaceholderPattern;
2366
2367
  let uidAttr;
2367
2368
  let uidPattern;
2368
2369
  // Create inline tags/text sets with custom elements
@@ -2396,6 +2397,7 @@ async function minifyHTML(value, options, partialMarkup) {
2396
2397
  if (!uidIgnore) {
2397
2398
  uidIgnore = uniqueId(value);
2398
2399
  const pattern = new RegExp('^' + uidIgnore + '([0-9]+)$');
2400
+ uidIgnorePlaceholderPattern = new RegExp('^<!--' + uidIgnore + '(\\d+)-->$');
2399
2401
  if (options.ignoreCustomComments) {
2400
2402
  options.ignoreCustomComments = options.ignoreCustomComments.slice();
2401
2403
  } else {
@@ -2820,6 +2822,79 @@ async function minifyHTML(value, options, partialMarkup) {
2820
2822
  optionalStartTag = '';
2821
2823
  optionalEndTag = '';
2822
2824
  }
2825
+
2826
+ // Optimize whitespace collapsing between consecutive `htmlmin:ignore` placeholder comments
2827
+ if (options.collapseWhitespace && text && uidIgnorePlaceholderPattern) {
2828
+ if (uidIgnorePlaceholderPattern.test(text)) {
2829
+ // Check if previous buffer items are: [ignore-placeholder, whitespace-only text]
2830
+ if (buffer.length >= 2) {
2831
+ const prevText = buffer[buffer.length - 1];
2832
+ const prevComment = buffer[buffer.length - 2];
2833
+
2834
+ // Check if previous item is whitespace-only and item before that is ignore-placeholder
2835
+ if (prevText && /^\s+$/.test(prevText) &&
2836
+ prevComment && uidIgnorePlaceholderPattern.test(prevComment)) {
2837
+ // Extract the index from both placeholders to check their content
2838
+ const currentMatch = text.match(uidIgnorePlaceholderPattern);
2839
+ const prevMatch = prevComment.match(uidIgnorePlaceholderPattern);
2840
+
2841
+ if (currentMatch && prevMatch) {
2842
+ const currentIndex = +currentMatch[1];
2843
+ const prevIndex = +prevMatch[1];
2844
+
2845
+ // Defensive bounds check to ensure indices are valid
2846
+ if (currentIndex < ignoredMarkupChunks.length && prevIndex < ignoredMarkupChunks.length) {
2847
+ const currentContent = ignoredMarkupChunks[currentIndex];
2848
+ const prevContent = ignoredMarkupChunks[prevIndex];
2849
+
2850
+ // Only collapse whitespace if both blocks contain HTML (start with `<`)
2851
+ // Don’t collapse if either contains plain text, as that would change meaning
2852
+ // Note: This check will match HTML comments (`<!-- … -->`), but the tag-name
2853
+ // regex below requires starting with a letter, so comments are intentionally
2854
+ // excluded by the `currentTagMatch && prevTagMatch` guard
2855
+ if (currentContent && prevContent && /^\s*</.test(currentContent) && /^\s*</.test(prevContent)) {
2856
+ // Extract tag names from the HTML content (excludes comments, processing instructions, etc.)
2857
+ const currentTagMatch = currentContent.match(/^\s*<([a-zA-Z][\w:-]*)/);
2858
+ const prevTagMatch = prevContent.match(/^\s*<([a-zA-Z][\w:-]*)/);
2859
+
2860
+ // Only collapse if both matched valid element tags (not comments/text)
2861
+ // and both tags are block-level (inline elements need whitespace preserved)
2862
+ if (currentTagMatch && prevTagMatch) {
2863
+ const currentTag = options.name(currentTagMatch[1]);
2864
+ const prevTag = options.name(prevTagMatch[1]);
2865
+
2866
+ // Don’t collapse between inline elements
2867
+ if (!inlineElements.has(currentTag) && !inlineElements.has(prevTag)) {
2868
+ // Collapse whitespace respecting context rules
2869
+ let collapsedText = prevText;
2870
+
2871
+ // Apply `collapseWhitespace` with appropriate context
2872
+ if (!stackNoTrimWhitespace.length && !stackNoCollapseWhitespace.length) {
2873
+ // Not in pre or other no-collapse context
2874
+ if (options.preserveLineBreaks && /[\n\r]/.test(prevText)) {
2875
+ // Preserve line break as single newline
2876
+ collapsedText = '\n';
2877
+ } else if (options.conservativeCollapse) {
2878
+ // Conservative mode: keep single space
2879
+ collapsedText = ' ';
2880
+ } else {
2881
+ // Aggressive mode: remove all whitespace
2882
+ collapsedText = '';
2883
+ }
2884
+ }
2885
+
2886
+ // Replace the whitespace in buffer
2887
+ buffer[buffer.length - 1] = collapsedText;
2888
+ }
2889
+ }
2890
+ }
2891
+ }
2892
+ }
2893
+ }
2894
+ }
2895
+ }
2896
+ }
2897
+
2823
2898
  buffer.push(text);
2824
2899
  },
2825
2900
  doctype: function (doctype) {
@@ -7505,6 +7505,7 @@ async function minifyHTML(value, options, partialMarkup) {
7505
7505
  const ignoredMarkupChunks = [];
7506
7506
  const ignoredCustomMarkupChunks = [];
7507
7507
  let uidIgnore;
7508
+ let uidIgnorePlaceholderPattern;
7508
7509
  let uidAttr;
7509
7510
  let uidPattern;
7510
7511
  // Create inline tags/text sets with custom elements
@@ -7538,6 +7539,7 @@ async function minifyHTML(value, options, partialMarkup) {
7538
7539
  if (!uidIgnore) {
7539
7540
  uidIgnore = uniqueId(value);
7540
7541
  const pattern = new RegExp('^' + uidIgnore + '([0-9]+)$');
7542
+ uidIgnorePlaceholderPattern = new RegExp('^<!--' + uidIgnore + '(\\d+)-->$');
7541
7543
  if (options.ignoreCustomComments) {
7542
7544
  options.ignoreCustomComments = options.ignoreCustomComments.slice();
7543
7545
  } else {
@@ -7962,6 +7964,79 @@ async function minifyHTML(value, options, partialMarkup) {
7962
7964
  optionalStartTag = '';
7963
7965
  optionalEndTag = '';
7964
7966
  }
7967
+
7968
+ // Optimize whitespace collapsing between consecutive `htmlmin:ignore` placeholder comments
7969
+ if (options.collapseWhitespace && text && uidIgnorePlaceholderPattern) {
7970
+ if (uidIgnorePlaceholderPattern.test(text)) {
7971
+ // Check if previous buffer items are: [ignore-placeholder, whitespace-only text]
7972
+ if (buffer.length >= 2) {
7973
+ const prevText = buffer[buffer.length - 1];
7974
+ const prevComment = buffer[buffer.length - 2];
7975
+
7976
+ // Check if previous item is whitespace-only and item before that is ignore-placeholder
7977
+ if (prevText && /^\s+$/.test(prevText) &&
7978
+ prevComment && uidIgnorePlaceholderPattern.test(prevComment)) {
7979
+ // Extract the index from both placeholders to check their content
7980
+ const currentMatch = text.match(uidIgnorePlaceholderPattern);
7981
+ const prevMatch = prevComment.match(uidIgnorePlaceholderPattern);
7982
+
7983
+ if (currentMatch && prevMatch) {
7984
+ const currentIndex = +currentMatch[1];
7985
+ const prevIndex = +prevMatch[1];
7986
+
7987
+ // Defensive bounds check to ensure indices are valid
7988
+ if (currentIndex < ignoredMarkupChunks.length && prevIndex < ignoredMarkupChunks.length) {
7989
+ const currentContent = ignoredMarkupChunks[currentIndex];
7990
+ const prevContent = ignoredMarkupChunks[prevIndex];
7991
+
7992
+ // Only collapse whitespace if both blocks contain HTML (start with `<`)
7993
+ // Don’t collapse if either contains plain text, as that would change meaning
7994
+ // Note: This check will match HTML comments (`<!-- … -->`), but the tag-name
7995
+ // regex below requires starting with a letter, so comments are intentionally
7996
+ // excluded by the `currentTagMatch && prevTagMatch` guard
7997
+ if (currentContent && prevContent && /^\s*</.test(currentContent) && /^\s*</.test(prevContent)) {
7998
+ // Extract tag names from the HTML content (excludes comments, processing instructions, etc.)
7999
+ const currentTagMatch = currentContent.match(/^\s*<([a-zA-Z][\w:-]*)/);
8000
+ const prevTagMatch = prevContent.match(/^\s*<([a-zA-Z][\w:-]*)/);
8001
+
8002
+ // Only collapse if both matched valid element tags (not comments/text)
8003
+ // and both tags are block-level (inline elements need whitespace preserved)
8004
+ if (currentTagMatch && prevTagMatch) {
8005
+ const currentTag = options.name(currentTagMatch[1]);
8006
+ const prevTag = options.name(prevTagMatch[1]);
8007
+
8008
+ // Don’t collapse between inline elements
8009
+ if (!inlineElements.has(currentTag) && !inlineElements.has(prevTag)) {
8010
+ // Collapse whitespace respecting context rules
8011
+ let collapsedText = prevText;
8012
+
8013
+ // Apply `collapseWhitespace` with appropriate context
8014
+ if (!stackNoTrimWhitespace.length && !stackNoCollapseWhitespace.length) {
8015
+ // Not in pre or other no-collapse context
8016
+ if (options.preserveLineBreaks && /[\n\r]/.test(prevText)) {
8017
+ // Preserve line break as single newline
8018
+ collapsedText = '\n';
8019
+ } else if (options.conservativeCollapse) {
8020
+ // Conservative mode: keep single space
8021
+ collapsedText = ' ';
8022
+ } else {
8023
+ // Aggressive mode: remove all whitespace
8024
+ collapsedText = '';
8025
+ }
8026
+ }
8027
+
8028
+ // Replace the whitespace in buffer
8029
+ buffer[buffer.length - 1] = collapsedText;
8030
+ }
8031
+ }
8032
+ }
8033
+ }
8034
+ }
8035
+ }
8036
+ }
8037
+ }
8038
+ }
8039
+
7965
8040
  buffer.push(text);
7966
8041
  },
7967
8042
  doctype: function (doctype) {
@@ -1 +1 @@
1
- {"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"AAqqEO,8BAJI,MAAM,YACN,eAAe,GACb,OAAO,CAAC,MAAM,CAAC,CAQ3B;;;;;;;;;;;;UA5oES,MAAM;YACN,MAAM;YACN,MAAM;mBACN,MAAM;iBACN,MAAM;kBACN,MAAM;;;;;;;;;;;;;4BAQN,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,qBAAqB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,KAAK,OAAO;;;;;;;wBAMjG,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,SAAS,EAAE,iBAAiB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,KAAK,OAAO;;;;;;;;oBAMhH,OAAO;;;;;;;;gCAOP,OAAO;;;;;;;;kCAOP,OAAO;;;;;;;;yBAOP,OAAO;;;;;;;;2BAOP,OAAO;;;;;;;;4BAOP,OAAO;;;;;;;2BAOP,OAAO;;;;;;;;uBAMP,MAAM,EAAE;;;;;;yBAOR,MAAM;;;;;;yBAKN,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE;;;;;;;4BAKlB,MAAM,EAAE;;;;;;;oCAMR,MAAM;;;;;;;qBAMN,OAAO;;;;;;;YAMP,OAAO;;;;;;;;2BAMP,MAAM,EAAE;;;;;;;;;4BAOR,MAAM,EAAE;;;;;;;+BAQR,OAAO;;;;;;;2BAMP,SAAS,CAAC,MAAM,CAAC;;;;;;uBAMjB,OAAO;;;;;;;;UAKP,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI;;;;;;;;qBAO1B,MAAM;;;;;;;oBAON,MAAM;;;;;;;;;;gBAMN,OAAO,GAAG,OAAO,CAAC,OAAO,cAAc,EAAE,gBAAgB,CAAC,OAAO,cAAc,EAAE,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;;;;;;;;;;eAS9J,OAAO,GAAG,OAAO,QAAQ,EAAE,aAAa,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;;;;;;;;;;iBASzG,OAAO,GAAG,MAAM,GAAG,OAAO,WAAW,EAAE,OAAO,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;;;;;;;;WAS7F,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM;;;;;;;+BAOxB,OAAO;;;;;;;;;;oBAMP,OAAO;;;;;;;;yBASP,OAAO;;;;;;;gCAOP,OAAO;;;;;;;;iCAMP,OAAO;;;;;;;;;;qBAOP,MAAM,EAAE;;;;;;;qBASR,IAAI,GAAG,GAAG;;;;;;;4BAMV,OAAO;;;;;;;;qBAMP,OAAO;;;;;;;;;4BAOP,OAAO,GAAG,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;;;;;;;;0BAQtD,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;gCAOP,MAAM,EAAE;;;;;;;;yBAyBR,OAAO;;;;;;;;gCAOP,OAAO;;;;;;;iCAOP,OAAO;;;;;;;oCAMP,OAAO;;;;;;;;;;0BAMP,OAAO;;;;;;;;;qBASP,OAAO,GAAG,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,IAAI,CAAC;;;;;;;;;oBAQzD,OAAO,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;;;;;;;;0BAQrC,OAAO;;;;;;;sBAOP,OAAO;;wBAnYkC,cAAc;0BAAd,cAAc;+BAAd,cAAc"}
1
+ {"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"AAgvEO,8BAJI,MAAM,YACN,eAAe,GACb,OAAO,CAAC,MAAM,CAAC,CAQ3B;;;;;;;;;;;;UAvtES,MAAM;YACN,MAAM;YACN,MAAM;mBACN,MAAM;iBACN,MAAM;kBACN,MAAM;;;;;;;;;;;;;4BAQN,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,qBAAqB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,KAAK,OAAO;;;;;;;wBAMjG,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,SAAS,EAAE,iBAAiB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,KAAK,OAAO;;;;;;;;oBAMhH,OAAO;;;;;;;;gCAOP,OAAO;;;;;;;;kCAOP,OAAO;;;;;;;;yBAOP,OAAO;;;;;;;;2BAOP,OAAO;;;;;;;;4BAOP,OAAO;;;;;;;2BAOP,OAAO;;;;;;;;uBAMP,MAAM,EAAE;;;;;;yBAOR,MAAM;;;;;;yBAKN,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE;;;;;;;4BAKlB,MAAM,EAAE;;;;;;;oCAMR,MAAM;;;;;;;qBAMN,OAAO;;;;;;;YAMP,OAAO;;;;;;;;2BAMP,MAAM,EAAE;;;;;;;;;4BAOR,MAAM,EAAE;;;;;;;+BAQR,OAAO;;;;;;;2BAMP,SAAS,CAAC,MAAM,CAAC;;;;;;uBAMjB,OAAO;;;;;;;;UAKP,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI;;;;;;;;qBAO1B,MAAM;;;;;;;oBAON,MAAM;;;;;;;;;;gBAMN,OAAO,GAAG,OAAO,CAAC,OAAO,cAAc,EAAE,gBAAgB,CAAC,OAAO,cAAc,EAAE,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;;;;;;;;;;eAS9J,OAAO,GAAG,OAAO,QAAQ,EAAE,aAAa,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;;;;;;;;;;iBASzG,OAAO,GAAG,MAAM,GAAG,OAAO,WAAW,EAAE,OAAO,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;;;;;;;;WAS7F,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM;;;;;;;+BAOxB,OAAO;;;;;;;;;;oBAMP,OAAO;;;;;;;;yBASP,OAAO;;;;;;;gCAOP,OAAO;;;;;;;;iCAMP,OAAO;;;;;;;;;;qBAOP,MAAM,EAAE;;;;;;;qBASR,IAAI,GAAG,GAAG;;;;;;;4BAMV,OAAO;;;;;;;;qBAMP,OAAO;;;;;;;;;4BAOP,OAAO,GAAG,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;;;;;;;;0BAQtD,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;gCAOP,MAAM,EAAE;;;;;;;;yBAyBR,OAAO;;;;;;;;gCAOP,OAAO;;;;;;;iCAOP,OAAO;;;;;;;oCAMP,OAAO;;;;;;;;;;0BAMP,OAAO;;;;;;;;;qBASP,OAAO,GAAG,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,IAAI,CAAC;;;;;;;;;oBAQzD,OAAO,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;;;;;;;;0BAQrC,OAAO;;;;;;;sBAOP,OAAO;;wBAnYkC,cAAc;0BAAd,cAAc;+BAAd,cAAc"}
package/package.json CHANGED
@@ -84,5 +84,5 @@
84
84
  "test:watch": "node --test --watch tests/*.spec.js"
85
85
  },
86
86
  "type": "module",
87
- "version": "4.9.0"
87
+ "version": "4.9.1"
88
88
  }
@@ -1657,6 +1657,7 @@ async function minifyHTML(value, options, partialMarkup) {
1657
1657
  const ignoredMarkupChunks = [];
1658
1658
  const ignoredCustomMarkupChunks = [];
1659
1659
  let uidIgnore;
1660
+ let uidIgnorePlaceholderPattern;
1660
1661
  let uidAttr;
1661
1662
  let uidPattern;
1662
1663
  // Create inline tags/text sets with custom elements
@@ -1690,6 +1691,7 @@ async function minifyHTML(value, options, partialMarkup) {
1690
1691
  if (!uidIgnore) {
1691
1692
  uidIgnore = uniqueId(value);
1692
1693
  const pattern = new RegExp('^' + uidIgnore + '([0-9]+)$');
1694
+ uidIgnorePlaceholderPattern = new RegExp('^<!--' + uidIgnore + '(\\d+)-->$');
1693
1695
  if (options.ignoreCustomComments) {
1694
1696
  options.ignoreCustomComments = options.ignoreCustomComments.slice();
1695
1697
  } else {
@@ -2114,6 +2116,79 @@ async function minifyHTML(value, options, partialMarkup) {
2114
2116
  optionalStartTag = '';
2115
2117
  optionalEndTag = '';
2116
2118
  }
2119
+
2120
+ // Optimize whitespace collapsing between consecutive `htmlmin:ignore` placeholder comments
2121
+ if (options.collapseWhitespace && text && uidIgnorePlaceholderPattern) {
2122
+ if (uidIgnorePlaceholderPattern.test(text)) {
2123
+ // Check if previous buffer items are: [ignore-placeholder, whitespace-only text]
2124
+ if (buffer.length >= 2) {
2125
+ const prevText = buffer[buffer.length - 1];
2126
+ const prevComment = buffer[buffer.length - 2];
2127
+
2128
+ // Check if previous item is whitespace-only and item before that is ignore-placeholder
2129
+ if (prevText && /^\s+$/.test(prevText) &&
2130
+ prevComment && uidIgnorePlaceholderPattern.test(prevComment)) {
2131
+ // Extract the index from both placeholders to check their content
2132
+ const currentMatch = text.match(uidIgnorePlaceholderPattern);
2133
+ const prevMatch = prevComment.match(uidIgnorePlaceholderPattern);
2134
+
2135
+ if (currentMatch && prevMatch) {
2136
+ const currentIndex = +currentMatch[1];
2137
+ const prevIndex = +prevMatch[1];
2138
+
2139
+ // Defensive bounds check to ensure indices are valid
2140
+ if (currentIndex < ignoredMarkupChunks.length && prevIndex < ignoredMarkupChunks.length) {
2141
+ const currentContent = ignoredMarkupChunks[currentIndex];
2142
+ const prevContent = ignoredMarkupChunks[prevIndex];
2143
+
2144
+ // Only collapse whitespace if both blocks contain HTML (start with `<`)
2145
+ // Don’t collapse if either contains plain text, as that would change meaning
2146
+ // Note: This check will match HTML comments (`<!-- … -->`), but the tag-name
2147
+ // regex below requires starting with a letter, so comments are intentionally
2148
+ // excluded by the `currentTagMatch && prevTagMatch` guard
2149
+ if (currentContent && prevContent && /^\s*</.test(currentContent) && /^\s*</.test(prevContent)) {
2150
+ // Extract tag names from the HTML content (excludes comments, processing instructions, etc.)
2151
+ const currentTagMatch = currentContent.match(/^\s*<([a-zA-Z][\w:-]*)/);
2152
+ const prevTagMatch = prevContent.match(/^\s*<([a-zA-Z][\w:-]*)/);
2153
+
2154
+ // Only collapse if both matched valid element tags (not comments/text)
2155
+ // and both tags are block-level (inline elements need whitespace preserved)
2156
+ if (currentTagMatch && prevTagMatch) {
2157
+ const currentTag = options.name(currentTagMatch[1]);
2158
+ const prevTag = options.name(prevTagMatch[1]);
2159
+
2160
+ // Don’t collapse between inline elements
2161
+ if (!inlineElements.has(currentTag) && !inlineElements.has(prevTag)) {
2162
+ // Collapse whitespace respecting context rules
2163
+ let collapsedText = prevText;
2164
+
2165
+ // Apply `collapseWhitespace` with appropriate context
2166
+ if (!stackNoTrimWhitespace.length && !stackNoCollapseWhitespace.length) {
2167
+ // Not in pre or other no-collapse context
2168
+ if (options.preserveLineBreaks && /[\n\r]/.test(prevText)) {
2169
+ // Preserve line break as single newline
2170
+ collapsedText = '\n';
2171
+ } else if (options.conservativeCollapse) {
2172
+ // Conservative mode: keep single space
2173
+ collapsedText = ' ';
2174
+ } else {
2175
+ // Aggressive mode: remove all whitespace
2176
+ collapsedText = '';
2177
+ }
2178
+ }
2179
+
2180
+ // Replace the whitespace in buffer
2181
+ buffer[buffer.length - 1] = collapsedText;
2182
+ }
2183
+ }
2184
+ }
2185
+ }
2186
+ }
2187
+ }
2188
+ }
2189
+ }
2190
+ }
2191
+
2117
2192
  buffer.push(text);
2118
2193
  },
2119
2194
  doctype: function (doctype) {