html-minifier-next 3.2.1 → 4.0.2

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/package.json CHANGED
@@ -6,9 +6,9 @@
6
6
  "bugs": "https://github.com/j9t/html-minifier-next/issues",
7
7
  "dependencies": {
8
8
  "change-case": "^4.1.2",
9
- "clean-css": "~5.3.3",
10
- "commander": "^14.0.1",
9
+ "commander": "^14.0.2",
11
10
  "entities": "^7.0.0",
11
+ "lightningcss": "^1.28.2",
12
12
  "relateurl": "^0.2.7",
13
13
  "terser": "^5.44.0"
14
14
  },
@@ -16,12 +16,12 @@
16
16
  "devDependencies": {
17
17
  "@commitlint/cli": "^20.1.0",
18
18
  "@eslint/js": "^9.37.0",
19
- "@rollup/plugin-commonjs": "^28.0.8",
19
+ "@rollup/plugin-commonjs": "^28.0.9",
20
20
  "@rollup/plugin-json": "^6.1.0",
21
21
  "@rollup/plugin-node-resolve": "^16.0.3",
22
22
  "@rollup/plugin-terser": "^0.4.4",
23
23
  "eslint": "^9.37.0",
24
- "rollup": "^4.52.4",
24
+ "rollup": "^4.52.5",
25
25
  "rollup-plugin-polyfill-node": "^0.13.0",
26
26
  "vite": "^7.1.12"
27
27
  },
@@ -78,5 +78,5 @@
78
78
  "test:watch": "node --test --watch tests/*.spec.js"
79
79
  },
80
80
  "type": "module",
81
- "version": "3.2.1"
81
+ "version": "4.0.2"
82
82
  }
@@ -1,4 +1,4 @@
1
- import CleanCSS from 'clean-css';
1
+ import { transform as transformCSS } from 'lightningcss';
2
2
  import { decodeHTMLStrict, decodeHTML } from 'entities';
3
3
  import RelateURL from 'relateurl';
4
4
  import { minify as terser } from 'terser';
@@ -390,12 +390,8 @@ function isContentSecurityPolicy(tag, attrs) {
390
390
  }
391
391
  }
392
392
 
393
- function ignoreCSS(id) {
394
- return '/* clean-css ignore:start */' + id + '/* clean-css ignore:end */';
395
- }
396
-
397
- // Wrap CSS declarations for CleanCSS > 3.x
398
- // See https://github.com/jakubpawlowicz/clean-css/issues/418
393
+ // Wrap CSS declarations for inline styles and media queries
394
+ // This ensures proper context for CSS minification
399
395
  function wrapCSS(text, type) {
400
396
  switch (type) {
401
397
  case 'inline':
@@ -458,7 +454,7 @@ const topLevelTags = new Set(['html', 'head', 'body']);
458
454
  const compactTags = new Set(['html', 'body']);
459
455
  const looseTags = new Set(['head', 'colgroup', 'caption']);
460
456
  const trailingTags = new Set(['dt', 'thead']);
461
- 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', 'section', 'select', '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']);
457
+ 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']);
462
458
 
463
459
  function canRemoveParentTag(optionalStartTag, tag) {
464
460
  switch (optionalStartTag) {
@@ -727,7 +723,7 @@ const processOptions = (inputOptions) => {
727
723
  return;
728
724
  }
729
725
 
730
- const cleanCssOptions = typeof option === 'object' ? option : {};
726
+ const lightningCssOptions = typeof option === 'object' ? option : {};
731
727
 
732
728
  options.minifyCSS = async function (text, type) {
733
729
  text = await replaceAsync(
@@ -748,17 +744,38 @@ const processOptions = (inputOptions) => {
748
744
 
749
745
  const inputCSS = wrapCSS(text, type);
750
746
 
751
- return new Promise((resolve) => {
752
- new CleanCSS(cleanCssOptions).minify(inputCSS, (_err, output) => {
753
- if (output.errors.length > 0) {
754
- output.errors.forEach(options.log);
755
- resolve(text);
756
- }
757
-
758
- const outputCSS = unwrapCSS(output.styles, type);
759
- resolve(outputCSS);
747
+ try {
748
+ const result = transformCSS({
749
+ filename: 'input.css',
750
+ code: Buffer.from(inputCSS),
751
+ minify: true,
752
+ errorRecovery: true,
753
+ ...lightningCssOptions
760
754
  });
761
- });
755
+
756
+ const outputCSS = unwrapCSS(result.code.toString(), type);
757
+
758
+ // If Lightning CSS removed significant content that looks like template syntax or UIDs, return original
759
+ // This preserves:
760
+ // 1. Template code like `<?php ?>`, `<%= %>`, `{{ }}`, etc. (contain `<` or `>` but not `CDATA`)
761
+ // 2. UIDs representing custom fragments (only lowercase letters and digits, no spaces)
762
+ // CDATA sections, HTML entities, and other invalid CSS are allowed to be removed
763
+ const isCDATA = text.includes('<![CDATA[');
764
+ const uidPattern = /[a-z0-9]{10,}/; // UIDs are long alphanumeric strings
765
+ const hasUID = uidPattern.test(text) && !isCDATA; // Exclude CDATA from UID detection
766
+ const looksLikeTemplate = (text.includes('<') || text.includes('>')) && !isCDATA;
767
+
768
+ // Preserve if output is empty and input had template syntax or UIDs
769
+ // This catches cases where Lightning CSS removed content that should be preserved
770
+ if (text.trim() && !outputCSS.trim() && (looksLikeTemplate || hasUID)) {
771
+ return text;
772
+ }
773
+
774
+ return outputCSS;
775
+ } catch (err) {
776
+ options.log && options.log(err);
777
+ return text;
778
+ }
762
779
  };
763
780
  } else if (key === 'minifyJS' && typeof option !== 'function') {
764
781
  if (!option) {
@@ -996,23 +1013,7 @@ async function minifyHTML(value, options, partialMarkup) {
996
1013
  return chunks[1] + uidAttr + index + uidAttr + chunks[2];
997
1014
  });
998
1015
 
999
- const ids = [];
1000
- new CleanCSS().minify(wrapCSS(text, type)).warnings.forEach(function (warning) {
1001
- const match = uidPattern.exec(warning);
1002
- if (match) {
1003
- const id = uidAttr + match[2] + uidAttr;
1004
- text = text.replace(id, ignoreCSS(id));
1005
- ids.push(id);
1006
- }
1007
- });
1008
-
1009
- return fn(text, type).then(chunk => {
1010
- ids.forEach(function (id) {
1011
- chunk = chunk.replace(ignoreCSS(id), id);
1012
- });
1013
-
1014
- return chunk;
1015
- });
1016
+ return fn(text, type);
1016
1017
  };
1017
1018
  })(options.minifyCSS);
1018
1019
  }
package/src/htmlparser.js CHANGED
@@ -58,7 +58,7 @@ let IS_REGEX_CAPTURING_BROKEN = false;
58
58
  const empty = new CaseInsensitiveSet(['area', 'base', 'basefont', 'br', 'col', 'embed', 'frame', 'hr', 'img', 'input', 'isindex', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
59
59
 
60
60
  // Inline elements
61
- const inline = new CaseInsensitiveSet(['a', 'abbr', 'acronym', 'applet', 'b', 'basefont', 'bdo', 'big', 'br', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'map', 'noscript', 'object', 'q', 's', 'samp', 'script', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'svg', 'textarea', 'tt', 'u', 'var']);
61
+ const inline = new CaseInsensitiveSet(['a', 'abbr', 'acronym', 'applet', 'b', 'basefont', 'bdo', 'big', 'br', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'map', 'noscript', 'object', 'q', 's', 'samp', 'script', 'select', 'selectedcontent', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'svg', 'textarea', 'tt', 'u', 'var']);
62
62
 
63
63
  // Elements that you can, intentionally, leave open
64
64
  // (and which close themselves)