html-minifier-next 4.17.0 → 4.17.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/README.md CHANGED
@@ -61,7 +61,7 @@ You can use a configuration file to specify options. The file can be either JSON
61
61
 
62
62
  **JavaScript module configuration example:**
63
63
 
64
- ```js
64
+ ```javascript
65
65
  module.exports = {
66
66
  collapseWhitespace: true,
67
67
  removeComments: true,
@@ -74,7 +74,7 @@ module.exports = {
74
74
 
75
75
  ESM with Node.js ≥16.14:
76
76
 
77
- ```js
77
+ ```javascript
78
78
  import { minify } from 'html-minifier-next';
79
79
 
80
80
  const result = await minify('<p title="example" id="moo">foo</p>', {
@@ -86,7 +86,7 @@ console.log(result); // “<p title=example id=moo>foo”
86
86
 
87
87
  CommonJS:
88
88
 
89
- ```js
89
+ ```javascript
90
90
  const { minify, getPreset } = require('html-minifier-next');
91
91
 
92
92
  (async () => {
@@ -188,7 +188,7 @@ When `minifyCSS` is set to `true`, HTML Minifier Next uses [Lightning CSS](https
188
188
 
189
189
  You can pass Lightning CSS configuration options by providing an object:
190
190
 
191
- ```js
191
+ ```javascript
192
192
  const result = await minify(html, {
193
193
  minifyCSS: {
194
194
  targets: {
@@ -211,7 +211,7 @@ Available Lightning CSS options when passed as an object:
211
211
 
212
212
  For advanced usage, you can also pass a function:
213
213
 
214
- ```js
214
+ ```javascript
215
215
  const result = await minify(html, {
216
216
  minifyCSS: function(text, type) {
217
217
  // `text`: CSS string to minify
@@ -227,7 +227,7 @@ When `minifyJS` is set to `true`, HTML Minifier Next uses [Terser](https://githu
227
227
 
228
228
  You can choose between different JS minifiers using the `engine` field:
229
229
 
230
- ```js
230
+ ```javascript
231
231
  const result = await minify(html, {
232
232
  minifyJS: {
233
233
  engine: 'swc', // Use SWC for faster minification
@@ -253,7 +253,7 @@ npm i @swc/core
253
253
 
254
254
  You can pass engine-specific configuration options:
255
255
 
256
- ```js
256
+ ```javascript
257
257
  // Using Terser with custom options
258
258
  const result = await minify(html, {
259
259
  minifyJS: {
@@ -273,7 +273,7 @@ const result = await minify(html, {
273
273
 
274
274
  For advanced usage, you can also pass a function:
275
275
 
276
- ```js
276
+ ```javascript
277
277
  const result = await minify(html, {
278
278
  minifyJS: function(text, inline) {
279
279
  // `text`: JavaScript string to minify
@@ -287,7 +287,7 @@ const result = await minify(html, {
287
287
 
288
288
  When `minifySVG` is set to `true`, HTML Minifier Next applies SVG-specific optimizations to SVG elements and their attributes. These optimizations are lightweight, fast, and safe:
289
289
 
290
- ```js
290
+ ```javascript
291
291
  const result = await minify(html, {
292
292
  minifySVG: true // Enable with default settings
293
293
  });
@@ -313,7 +313,7 @@ What gets optimized:
313
313
 
314
314
  You can customize the optimization behavior by providing an options object:
315
315
 
316
- ```js
316
+ ```javascript
317
317
  const result = await minify(html, {
318
318
  minifySVG: {
319
319
  precision: 2, // Use 2 decimal places instead of 3
package/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+
2
3
  /**
3
4
  * html-minifier-next CLI tool
4
5
  *
@@ -324,7 +325,7 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
324
325
  if (presetName) {
325
326
  const preset = getPreset(presetName);
326
327
  if (!preset) {
327
- fatal(`Unknown preset "${presetName}". Available presets: ${getPresetNames().join(', ')}`);
328
+ fatal(`Unknown preset “${presetName}”. Available presets: ${getPresetNames().join(', ')}`);
328
329
  }
329
330
  Object.assign(options, preset);
330
331
  }
@@ -778,7 +778,6 @@ const presets = {
778
778
  comprehensive: {
779
779
  caseSensitive: true,
780
780
  collapseBooleanAttributes: true,
781
- collapseInlineTagWhitespace: true,
782
781
  collapseWhitespace: true,
783
782
  continueOnParseError: true,
784
783
  decodeEntities: true,
@@ -1830,9 +1829,25 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
1830
1829
  });
1831
1830
  };
1832
1831
 
1832
+ // Apply preset first if specified (so user options can override preset values)
1833
+ if (inputOptions.preset) {
1834
+ const preset = getPreset(inputOptions.preset);
1835
+ if (preset) {
1836
+ Object.assign(options, preset);
1837
+ } else {
1838
+ const available = getPresetNames().join(', ');
1839
+ console.warn(`HTML Minifier Next: Unknown preset “${inputOptions.preset}”. Available presets: ${available}`);
1840
+ }
1841
+ }
1842
+
1833
1843
  Object.keys(inputOptions).forEach(function (key) {
1834
1844
  const option = inputOptions[key];
1835
1845
 
1846
+ // Skip preset key—it’s already been processed
1847
+ if (key === 'preset') {
1848
+ return;
1849
+ }
1850
+
1836
1851
  if (key === 'caseSensitive') {
1837
1852
  if (option) {
1838
1853
  options.name = identity;
@@ -3914,6 +3929,20 @@ async function minifyHTML(value, options, partialMarkup) {
3914
3929
  text = entities.decodeHTML(text);
3915
3930
  }
3916
3931
  }
3932
+ // Trim outermost newline-based whitespace inside `pre`/`textarea` elements
3933
+ // This removes trailing newlines often added by template engines before closing tags
3934
+ // Only trims single trailing newlines (multiple newlines are likely intentional formatting)
3935
+ if (options.collapseWhitespace && stackNoTrimWhitespace.length) {
3936
+ const topTag = stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1];
3937
+ if (stackNoTrimWhitespace.includes('pre') || stackNoTrimWhitespace.includes('textarea')) {
3938
+ // Trim trailing whitespace only if it ends with a single newline (not multiple)
3939
+ // Multiple newlines are likely intentional formatting, single newline is often a template artifact
3940
+ // Treat CRLF (`\r\n`), CR (`\r`), and LF (`\n`) as single line-ending units
3941
+ if (nextTag && nextTag === '/' + topTag && /[^\r\n](?:\r\n|\r|\n)[ \t]*$/.test(text)) {
3942
+ text = text.replace(/(?:\r\n|\r|\n)[ \t]*$/, '');
3943
+ }
3944
+ }
3945
+ }
3917
3946
  if (options.collapseWhitespace) {
3918
3947
  if (!stackNoTrimWhitespace.length) {
3919
3948
  if (prevTag === 'comment') {
@@ -3390,7 +3390,6 @@ const presets = {
3390
3390
  comprehensive: {
3391
3391
  caseSensitive: true,
3392
3392
  collapseBooleanAttributes: true,
3393
- collapseInlineTagWhitespace: true,
3394
3393
  collapseWhitespace: true,
3395
3394
  continueOnParseError: true,
3396
3395
  decodeEntities: true,
@@ -6972,9 +6971,25 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
6972
6971
  });
6973
6972
  };
6974
6973
 
6974
+ // Apply preset first if specified (so user options can override preset values)
6975
+ if (inputOptions.preset) {
6976
+ const preset = getPreset(inputOptions.preset);
6977
+ if (preset) {
6978
+ Object.assign(options, preset);
6979
+ } else {
6980
+ const available = getPresetNames().join(', ');
6981
+ console.warn(`HTML Minifier Next: Unknown preset “${inputOptions.preset}”. Available presets: ${available}`);
6982
+ }
6983
+ }
6984
+
6975
6985
  Object.keys(inputOptions).forEach(function (key) {
6976
6986
  const option = inputOptions[key];
6977
6987
 
6988
+ // Skip preset key—it’s already been processed
6989
+ if (key === 'preset') {
6990
+ return;
6991
+ }
6992
+
6978
6993
  if (key === 'caseSensitive') {
6979
6994
  if (option) {
6980
6995
  options.name = identity;
@@ -9056,6 +9071,20 @@ async function minifyHTML(value, options, partialMarkup) {
9056
9071
  text = decodeHTML(text);
9057
9072
  }
9058
9073
  }
9074
+ // Trim outermost newline-based whitespace inside `pre`/`textarea` elements
9075
+ // This removes trailing newlines often added by template engines before closing tags
9076
+ // Only trims single trailing newlines (multiple newlines are likely intentional formatting)
9077
+ if (options.collapseWhitespace && stackNoTrimWhitespace.length) {
9078
+ const topTag = stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1];
9079
+ if (stackNoTrimWhitespace.includes('pre') || stackNoTrimWhitespace.includes('textarea')) {
9080
+ // Trim trailing whitespace only if it ends with a single newline (not multiple)
9081
+ // Multiple newlines are likely intentional formatting, single newline is often a template artifact
9082
+ // Treat CRLF (`\r\n`), CR (`\r`), and LF (`\n`) as single line-ending units
9083
+ if (nextTag && nextTag === '/' + topTag && /[^\r\n](?:\r\n|\r|\n)[ \t]*$/.test(text)) {
9084
+ text = text.replace(/(?:\r\n|\r|\n)[ \t]*$/, '');
9085
+ }
9086
+ }
9087
+ }
9059
9088
  if (options.collapseWhitespace) {
9060
9089
  if (!stackNoTrimWhitespace.length) {
9061
9090
  if (prevTag === 'comment') {
@@ -1 +1 @@
1
- {"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../../src/lib/options.js"],"names":[],"mappings":"AAWA,6DAUC;AAID;;;;;;;;;;GAUG;AACH,6CAVW,OAAO,CAAC,eAAe,CAAC,0FAEhC;IAAuB,eAAe;IACf,SAAS;IACT,MAAM;CAA2B,GAI9C,eAAe,CA4U3B"}
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../../src/lib/options.js"],"names":[],"mappings":"AAYA,6DAUC;AAID;;;;;;;;;;GAUG;AACH,6CAVW,OAAO,CAAC,eAAe,CAAC,0FAEhC;IAAuB,eAAe;IACf,SAAS;IACT,MAAM;CAA2B,GAI9C,eAAe,CA4V3B"}
@@ -28,7 +28,6 @@ export namespace presets {
28
28
  export let caseSensitive: boolean;
29
29
  let collapseBooleanAttributes_1: boolean;
30
30
  export { collapseBooleanAttributes_1 as collapseBooleanAttributes };
31
- export let collapseInlineTagWhitespace: boolean;
32
31
  let collapseWhitespace_1: boolean;
33
32
  export { collapseWhitespace_1 as collapseWhitespace };
34
33
  let continueOnParseError_1: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../../src/presets.js"],"names":[],"mappings":"AAiDA;;;;GAIG;AACH,gCAHW,MAAM,GACJ,MAAM,GAAC,IAAI,CAMvB;AAED;;;GAGG;AACH,kCAFa,MAAM,EAAE,CAIpB"}
1
+ {"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../../src/presets.js"],"names":[],"mappings":"AAgDA;;;;GAIG;AACH,gCAHW,MAAM,GACJ,MAAM,GAAC,IAAI,CAMvB;AAED;;;GAGG;AACH,kCAFa,MAAM,EAAE,CAIpB"}
package/package.json CHANGED
@@ -15,18 +15,18 @@
15
15
  },
16
16
  "description": "Super-configurable, well-tested, JavaScript-based HTML minifier (enhanced successor of HTML Minifier)",
17
17
  "devDependencies": {
18
- "@commitlint/cli": "^20.2.0",
18
+ "@commitlint/cli": "^20.3.0",
19
19
  "@eslint/js": "^9.39.2",
20
20
  "@rollup/plugin-commonjs": "^29.0.0",
21
21
  "@rollup/plugin-json": "^6.1.0",
22
22
  "@rollup/plugin-node-resolve": "^16.0.3",
23
23
  "@rollup/plugin-terser": "^0.4.4",
24
- "@swc/core": "^1.15.7",
24
+ "@swc/core": "^1.15.8",
25
25
  "eslint": "^9.39.2",
26
- "rollup": "^4.54.0",
26
+ "rollup": "^4.55.1",
27
27
  "rollup-plugin-polyfill-node": "^0.13.0",
28
28
  "typescript": "^5.9.3",
29
- "vite": "^7.3.0"
29
+ "vite": "^7.3.1"
30
30
  },
31
31
  "exports": {
32
32
  ".": {
@@ -98,5 +98,5 @@
98
98
  },
99
99
  "type": "module",
100
100
  "types": "./dist/types/htmlminifier.d.ts",
101
- "version": "4.17.0"
101
+ "version": "4.17.2"
102
102
  }
@@ -1088,6 +1088,20 @@ async function minifyHTML(value, options, partialMarkup) {
1088
1088
  text = decodeHTML(text);
1089
1089
  }
1090
1090
  }
1091
+ // Trim outermost newline-based whitespace inside `pre`/`textarea` elements
1092
+ // This removes trailing newlines often added by template engines before closing tags
1093
+ // Only trims single trailing newlines (multiple newlines are likely intentional formatting)
1094
+ if (options.collapseWhitespace && stackNoTrimWhitespace.length) {
1095
+ const topTag = stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1];
1096
+ if (stackNoTrimWhitespace.includes('pre') || stackNoTrimWhitespace.includes('textarea')) {
1097
+ // Trim trailing whitespace only if it ends with a single newline (not multiple)
1098
+ // Multiple newlines are likely intentional formatting, single newline is often a template artifact
1099
+ // Treat CRLF (`\r\n`), CR (`\r`), and LF (`\n`) as single line-ending units
1100
+ if (nextTag && nextTag === '/' + topTag && /[^\r\n](?:\r\n|\r|\n)[ \t]*$/.test(text)) {
1101
+ text = text.replace(/(?:\r\n|\r|\n)[ \t]*$/, '');
1102
+ }
1103
+ }
1104
+ }
1091
1105
  if (options.collapseWhitespace) {
1092
1106
  if (!stackNoTrimWhitespace.length) {
1093
1107
  if (prevTag === 'comment') {
@@ -6,6 +6,7 @@ import { RE_TRAILING_SEMICOLON } from './constants.js';
6
6
  import { canCollapseWhitespace, canTrimWhitespace } from './whitespace.js';
7
7
  import { wrapCSS, unwrapCSS } from './content.js';
8
8
  import { getSVGMinifierOptions } from './svg.js';
9
+ import { getPreset, getPresetNames } from '../presets.js';
9
10
 
10
11
  // Helper functions
11
12
 
@@ -84,9 +85,25 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
84
85
  });
85
86
  };
86
87
 
88
+ // Apply preset first if specified (so user options can override preset values)
89
+ if (inputOptions.preset) {
90
+ const preset = getPreset(inputOptions.preset);
91
+ if (preset) {
92
+ Object.assign(options, preset);
93
+ } else {
94
+ const available = getPresetNames().join(', ');
95
+ console.warn(`HTML Minifier Next: Unknown preset “${inputOptions.preset}”. Available presets: ${available}`);
96
+ }
97
+ }
98
+
87
99
  Object.keys(inputOptions).forEach(function (key) {
88
100
  const option = inputOptions[key];
89
101
 
102
+ // Skip preset key—it’s already been processed
103
+ if (key === 'preset') {
104
+ return;
105
+ }
106
+
90
107
  if (key === 'caseSensitive') {
91
108
  if (option) {
92
109
  options.name = identity;
package/src/presets.js CHANGED
@@ -24,7 +24,6 @@ export const presets = {
24
24
  comprehensive: {
25
25
  caseSensitive: true,
26
26
  collapseBooleanAttributes: true,
27
- collapseInlineTagWhitespace: true,
28
27
  collapseWhitespace: true,
29
28
  continueOnParseError: true,
30
29
  decodeEntities: true,