html-minifier-next 6.1.5 → 6.2.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
@@ -129,7 +129,7 @@ html-minifier-next --preset conservative --remove-empty-attributes input.html
129
129
 
130
130
  Most of the options are disabled by default. Experiment and find what works best for you and your project.
131
131
 
132
- Options can be used in config files (camelCase) or via CLI flags (kebab-case with `--` prefix). Options that default to `true` use `--no-` prefix in CLI to disable them.
132
+ Options can be used in config files (camelCase) or via CLI flags (kebab-case with `--` prefix). Boolean options generally support both `--option-name` to enable and `--no-option-name` to disable, so you can override a preset or config file from the command line. (Exception: Options whose name already starts with `no-`, such as `noNewlinesBeforeTagClose`, only expose the `--no-…` CLI flag.)
133
133
 
134
134
  | Option (config/CLI) | Description | Default |
135
135
  | --- | --- | --- |
@@ -142,7 +142,7 @@ Options can be used in config files (camelCase) or via CLI flags (kebab-case wit
142
142
  | `collapseInlineTagWhitespace`<br>`--collapse-inline-tag-whitespace` | Collapse whitespace more aggressively between inline elements—use with `collapseWhitespace: true` | `false` |
143
143
  | `collapseWhitespace`<br>`--collapse-whitespace` | [Collapse whitespace that contributes to text nodes in a document tree](https://perfectionkills.com/experimenting-with-html-minifier/#collapse_whitespace) | `false` |
144
144
  | `conservativeCollapse`<br>`--conservative-collapse` | Always collapse to one space (never remove it entirely)—use with `collapseWhitespace: true` | `false` |
145
- | `continueOnMinifyError`<br>`--no-continue-on-minify-error` | Continue on minification errors; when `false`, minification errors throw and abort processing | `true` |
145
+ | `continueOnMinifyError`<br>`--continue-on-minify-error`<br>`--no-continue-on-minify-error` | Continue on minification errors; when `false`, minification errors throw and abort processing | `true` |
146
146
  | `continueOnParseError`<br>`--continue-on-parse-error` | [Handle parse errors](https://html.spec.whatwg.org/multipage/parsing.html#parse-errors) instead of aborting | `false` |
147
147
  | `customAttrAssign`<br>`--custom-attr-assign` | Array of regexes that allow to support custom attribute assign expressions (e.g., `<div flex?="{{mode != cover}}"></div>`) | `[]` |
148
148
  | `customAttrCollapse`<br>`--custom-attr-collapse` | Regex that specifies custom attribute to strip newlines from (e.g., `/ng-class/`) | `undefined` |
@@ -603,7 +603,7 @@ Parameters:
603
603
 
604
604
  ## Acknowledgements
605
605
 
606
- 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).
606
+ With many thanks to the previous authors of and contributors to 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), [Jonas Geiler](https://github.com/jonasgeiler), and [Chris Morgan](https://github.com/chris-morgan)!
607
607
 
608
608
  ***
609
609
 
package/cli.js CHANGED
@@ -34,12 +34,22 @@ import { pathToFileURL } from 'url';
34
34
  import os from 'os';
35
35
  import readline from 'readline';
36
36
  import { createRequire } from 'module';
37
- import { Command } from 'commander';
37
+ import { Command, Option } from 'commander';
38
38
 
39
39
  // Simple case conversion for CLI option names (ASCII-only, no Unicode needed)
40
40
  const paramCase = (str) => str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
41
41
  const camelCase = (str) => paramCase(str).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
42
42
 
43
+ // Commander derives its internal option key by applying paramCase then camelCase to the flag name,
44
+ // stripping a leading `no-` first for negated flags (e.g., `--no-foo-bar` → `fooBar`);
45
+ // because option definition keys may differ from the result of that round-trip (e.g.,
46
+ // `minifyURLs` → Commander key `minifyUrls`, `noNewlinesBeforeTagClose` → `newlinesBeforeTagClose`),
47
+ // `commanderOptionKey` uses the same paramCase + camelCase path to compute the key Commander will use
48
+ const commanderOptionKey = (key) => {
49
+ const pc = paramCase(key);
50
+ return pc.startsWith('no-') ? camelCase(pc.slice(3)) : camelCase(pc);
51
+ };
52
+
43
53
  // Lazy-load HMN to reduce CLI cold-start overhead
44
54
  import { getPreset, getPresetNames } from './src/presets.js';
45
55
  import { parseRegExp } from './src/lib/utils.js';
@@ -140,20 +150,31 @@ const typeParsers = {
140
150
  // Configure command-line flags from shared option definitions
141
151
  const mainOptionKeys = Object.keys(optionDefinitions);
142
152
  mainOptionKeys.forEach(function (key) {
143
- const { description, type } = optionDefinitions[key];
153
+ const { description, descriptionAffirmative, type } = optionDefinitions[key];
154
+ const flag = paramCase(key);
144
155
  if (type === 'invertedBoolean') {
145
- program.option('--no-' + paramCase(key), description);
156
+ // The positive form (to re-enable after a preset/config disables it) is hidden from
157
+ // help—the footer note covers the convention; the negative form is the primary use case
158
+ program.addOption(new Option('--' + flag, descriptionAffirmative ?? 'Enable --' + flag).hideHelp());
159
+ program.option('--no-' + flag, description);
146
160
  } else if (type === 'boolean') {
147
- program.option('--' + paramCase(key), description);
161
+ program.option('--' + flag, description);
162
+ // The negation form is hidden from help—the footer note covers the convention;
163
+ // skip options whose flag already starts with `no-` (currently only
164
+ // `noNewlinesBeforeTagClose`), as `--no-no-X` is not usable
165
+ if (!flag.startsWith('no-')) {
166
+ program.addOption(new Option('--no-' + flag, 'Disable --' + flag).hideHelp());
167
+ }
148
168
  } else {
149
- const flag = '--' + paramCase(key) + (type === 'json' ? ' [value]' : ' <value>');
169
+ const cliFlag = '--' + flag + (type === 'json' ? ' [value]' : ' <value>');
150
170
  const parser = type === 'int' ? typeParsers.int(key) : typeParsers[type];
151
- program.option(flag, description, parser);
171
+ program.option(cliFlag, description, parser);
152
172
  }
153
173
  });
154
174
  program.option('-o --output <file>', 'Specify output file (reads from file arguments or STDIN; outputs to STDOUT if not specified)');
155
175
  program.option('-v --verbose', 'Show detailed processing information');
156
176
  program.option('-d --dry', 'Dry run: Process and report statistics without writing output');
177
+ program.addHelpText('after', '\nBoolean options support a `--no-<flag>` form to disable them, overriding a preset or config file (e.g., `--preset=comprehensive --no-collapse-whitespace`).');
157
178
 
158
179
  // Lazy import wrapper for HMN
159
180
  let minifyFnPromise;
@@ -398,9 +419,14 @@ program.helpOption('-h, --help', 'Display help for command');
398
419
 
399
420
  // 3. Apply CLI options (overrides config and preset)
400
421
  mainOptionKeys.forEach(function (key) {
401
- const param = programOptions[camelCase(key)];
402
- if (typeof param !== 'undefined') {
403
- options[key] = param;
422
+ const { type } = optionDefinitions[key];
423
+ const ck = commanderOptionKey(key);
424
+ if (program.getOptionValueSource(ck) === 'cli') {
425
+ const val = programOptions[ck];
426
+ // For boolean options whose param-case name starts with `no-`, Commander treats
427
+ // the flag as a negation and stores the inverted value under the stripped key;
428
+ // invert back so the option definition key gets the intended value
429
+ options[key] = (type === 'boolean' && paramCase(key).startsWith('no-')) ? !val : val;
404
430
  }
405
431
  });
406
432
 
@@ -413,7 +439,7 @@ program.helpOption('-h, --help', 'Display help for command');
413
439
  console.error(`Using preset: ${presetName}`);
414
440
  }
415
441
  const activeOptions = Object.entries(minifierOptions)
416
- .filter(([k]) => program.getOptionValueSource(camelCase(k)) === 'cli')
442
+ .filter(([k]) => program.getOptionValueSource(commanderOptionKey(k)) === 'cli')
417
443
  .map(([k, v]) => (typeof v === 'boolean' ? (v ? k : `no-${k}`) : k));
418
444
  if (activeOptions.length > 0) {
419
445
  console.error('CLI options: ' + activeOptions.join(', '));
@@ -1429,15 +1429,15 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, prevAttrs, nextAttrs, op
1429
1429
 
1430
1430
  // Collapse/trim whitespace for given tag
1431
1431
 
1432
- const noCollapseWsTags = new Set(['script', 'style', 'pre', 'textarea']);
1433
- const noTrimWsTags = new Set(['pre', 'textarea']);
1432
+ const noCollapseWhitespaceTags = new Set(['script', 'style', 'pre', 'textarea']);
1433
+ const noTrimWhitespaceTags = new Set(['pre', 'textarea']);
1434
1434
 
1435
1435
  function canCollapseWhitespace(tag) {
1436
- return !noCollapseWsTags.has(tag);
1436
+ return !noCollapseWhitespaceTags.has(tag);
1437
1437
  }
1438
1438
 
1439
1439
  function canTrimWhitespace(tag) {
1440
- return !noTrimWsTags.has(tag);
1440
+ return !noTrimWhitespaceTags.has(tag);
1441
1441
  }
1442
1442
 
1443
1443
  /**
@@ -2366,12 +2366,18 @@ function hasAttrName(name, attrs) {
2366
2366
 
2367
2367
  // Cleaners
2368
2368
 
2369
+ const collapseAttributeWhitespaceExempt = new Set(['pattern', 'placeholder', 'title']);
2370
+ // `value` whitespace matters only on form-submission and machine-readable elements
2371
+ const valueWhitespaceExemptElements = new Set(['button', 'data', 'input', 'option', 'param']);
2372
+
2369
2373
  // Returns the cleaned attribute value directly (sync) or as a Promise (async);
2370
2374
  // callers must handle both cases—use `isThenable()` to distinguish
2371
2375
  function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTMLSelf) {
2376
+ const isEventAttr = isEventAttribute(attrName, options);
2377
+
2372
2378
  // Apply early whitespace normalization if enabled
2373
2379
  // Preserves special spaces (no-break space, hair space, etc.) for consistency with `collapseWhitespace`
2374
- if (options.collapseAttributeWhitespace) {
2380
+ if (options.collapseAttributeWhitespace && !collapseAttributeWhitespaceExempt.has(attrName) && !(attrName === 'value' && valueWhitespaceExemptElements.has(tag)) && !isEventAttr) {
2375
2381
  // Fast path: Only process if whitespace exists (avoids regex overhead on clean values)
2376
2382
  if (RE_ATTR_WS_CHECK.test(attrValue)) {
2377
2383
  // Two-pass approach (faster than single-pass with callback)
@@ -2381,7 +2387,7 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
2381
2387
  }
2382
2388
  }
2383
2389
 
2384
- if (isEventAttribute(attrName, options)) {
2390
+ if (isEventAttr) {
2385
2391
  attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, '');
2386
2392
  const result = options.minifyJS(attrValue, true);
2387
2393
  if (isThenable(result)) {
@@ -1 +1 @@
1
- {"version":3,"file":"attributes.d.ts","sourceRoot":"","sources":["../../../src/lib/attributes.js"],"names":[],"mappings":"AAmCA,mEAOC;AAED,uEAWC;AAED,8DAGC;AAED,4EAOC;AAgCD,mGAuCC;AAED,mEAGC;AAED,qEAGC;AAED,kEAWC;AAED,sEAGC;AAED,8DAWC;AAED,2EAIC;AAmBD,qEAGC;AAgBD,wEAGC;AAED,sEAUC;AAED,2EAEC;AAED,2DAEC;AAED,8DAUC;AAED,uEAUC;AAED,oGASC;AAED,4DAOC;AAMD,iIA0KC;AAwBD,mGAYC;AA0CD,6GAuHC;AAllBD;;;;;;;GAOG;AACH,mEAHW,OAAO,SAuBjB"}
1
+ {"version":3,"file":"attributes.d.ts","sourceRoot":"","sources":["../../../src/lib/attributes.js"],"names":[],"mappings":"AAmCA,mEAOC;AAED,uEAWC;AAED,8DAGC;AAED,4EAOC;AAgCD,mGAuCC;AAED,mEAGC;AAED,qEAGC;AAED,kEAWC;AAED,sEAGC;AAED,8DAWC;AAED,2EAIC;AAmBD,qEAGC;AAgBD,wEAGC;AAED,sEAUC;AAED,2EAEC;AAED,2DAEC;AAED,8DAUC;AAED,uEAUC;AAED,oGASC;AAED,4DAOC;AAUD,iIA4KC;AAwBD,mGAYC;AA0CD,6GAuHC;AAxlBD;;;;;;;GAOG;AACH,mEAHW,OAAO,SAuBjB"}
@@ -36,6 +36,7 @@ export namespace optionDefinitions {
36
36
  namespace continueOnMinifyError {
37
37
  let description_6: string;
38
38
  export { description_6 as description };
39
+ export let descriptionAffirmative: string;
39
40
  let type_6: string;
40
41
  export { type_6 as type };
41
42
  }
package/package.json CHANGED
@@ -18,11 +18,11 @@
18
18
  "@rollup/plugin-commonjs": "^29.0.2",
19
19
  "@rollup/plugin-json": "^6.1.0",
20
20
  "@rollup/plugin-node-resolve": "^16.0.3",
21
- "@swc/core": "^1.15.21",
21
+ "@swc/core": "^1.15.30",
22
22
  "eslint": "^10.2.0",
23
- "rollup": "^4.60.0",
23
+ "rollup": "^4.60.2",
24
24
  "rollup-plugin-polyfill-node": "^0.13.0",
25
- "typescript": "^6.0.2",
25
+ "typescript": "^6.0.3",
26
26
  "vite": "^8.0.8"
27
27
  },
28
28
  "exports": {
@@ -96,5 +96,5 @@
96
96
  },
97
97
  "type": "module",
98
98
  "types": "./dist/types/htmlminifier.d.ts",
99
- "version": "6.1.5"
99
+ "version": "6.2.1"
100
100
  }
@@ -294,12 +294,18 @@ function hasAttrName(name, attrs) {
294
294
 
295
295
  // Cleaners
296
296
 
297
+ const collapseAttributeWhitespaceExempt = new Set(['pattern', 'placeholder', 'title']);
298
+ // `value` whitespace matters only on form-submission and machine-readable elements
299
+ const valueWhitespaceExemptElements = new Set(['button', 'data', 'input', 'option', 'param']);
300
+
297
301
  // Returns the cleaned attribute value directly (sync) or as a Promise (async);
298
302
  // callers must handle both cases—use `isThenable()` to distinguish
299
303
  function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTMLSelf) {
304
+ const isEventAttr = isEventAttribute(attrName, options);
305
+
300
306
  // Apply early whitespace normalization if enabled
301
307
  // Preserves special spaces (no-break space, hair space, etc.) for consistency with `collapseWhitespace`
302
- if (options.collapseAttributeWhitespace) {
308
+ if (options.collapseAttributeWhitespace && !collapseAttributeWhitespaceExempt.has(attrName) && !(attrName === 'value' && valueWhitespaceExemptElements.has(tag)) && !isEventAttr) {
303
309
  // Fast path: Only process if whitespace exists (avoids regex overhead on clean values)
304
310
  if (RE_ATTR_WS_CHECK.test(attrValue)) {
305
311
  // Two-pass approach (faster than single-pass with callback)
@@ -309,7 +315,7 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
309
315
  }
310
316
  }
311
317
 
312
- if (isEventAttribute(attrName, options)) {
318
+ if (isEventAttr) {
313
319
  attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, '');
314
320
  const result = options.minifyJS(attrValue, true);
315
321
  if (isThenable(result)) {
@@ -27,6 +27,7 @@ const optionDefinitions = {
27
27
  },
28
28
  continueOnMinifyError: {
29
29
  description: 'Abort on minification errors',
30
+ descriptionAffirmative: 'Continue on minification errors',
30
31
  type: 'invertedBoolean'
31
32
  },
32
33
  continueOnParseError: {
@@ -211,15 +211,15 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, prevAttrs, nextAttrs, op
211
211
 
212
212
  // Collapse/trim whitespace for given tag
213
213
 
214
- const noCollapseWsTags = new Set(['script', 'style', 'pre', 'textarea']);
215
- const noTrimWsTags = new Set(['pre', 'textarea']);
214
+ const noCollapseWhitespaceTags = new Set(['script', 'style', 'pre', 'textarea']);
215
+ const noTrimWhitespaceTags = new Set(['pre', 'textarea']);
216
216
 
217
217
  function canCollapseWhitespace(tag) {
218
- return !noCollapseWsTags.has(tag);
218
+ return !noCollapseWhitespaceTags.has(tag);
219
219
  }
220
220
 
221
221
  function canTrimWhitespace(tag) {
222
- return !noTrimWsTags.has(tag);
222
+ return !noTrimWhitespaceTags.has(tag);
223
223
  }
224
224
 
225
225
  // Exports