html-minifier-next 7.5.2 → 7.5.3

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
@@ -1,6 +1,6 @@
1
1
  # HTML Minifier Next
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/html-minifier-next.svg)](https://www.npmjs.com/package/html-minifier-next) [![Build status](https://github.com/j9t/html-minifier-next/workflows/Tests/badge.svg)](https://github.com/j9t/html-minifier-next/actions) [![Socket](https://badge.socket.dev/npm/package/html-minifier-next)](https://socket.dev/npm/package/html-minifier-next) [![GitHub Sponsors](https://badgen.net/static/Support/Open%20Source/cyan)](https://github.com/j9t/html-minifier-next?sponsor=1)
3
+ [![npm version](https://img.shields.io/npm/v/html-minifier-next.svg)](https://www.npmjs.com/package/html-minifier-next) [![Build status](https://github.com/j9t/html-minifier-next/workflows/Tests/badge.svg)](https://github.com/j9t/html-minifier-next/actions) [![Socket](https://badge.socket.dev/npm/package/html-minifier-next)](https://socket.dev/npm/package/html-minifier-next) [![GitHub Sponsors](https://badgen.net/static/Support/Open%20Source/cyan)](https://github.com/sponsors/j9t)
4
4
 
5
5
  Your web page optimization precision tool: HTML Minifier Next (HMN) is a **highly effective, super-configurable, well-tested HTML minifier**, written in JavaScript, that also handles in-document CSS, JavaScript, and SVG minification.
6
6
 
@@ -40,13 +40,13 @@ Use `npx html-minifier-next --help` to check all available options:
40
40
  | `--output <file>`, `-o <file>` | Specify output file (reads from `--input` file argument or STDIN; outputs to STDOUT if not specified) | File to file: `npx html-minifier-next input.html -o output.html`<br>File to file (explicit): `npx html-minifier-next -i input.html -o output.html`<br>Pipe to file: `cat input.html \| npx html-minifier-next -o output.html`<br>File to STDOUT: `npx html-minifier-next input.html` |
41
41
  | `--file-ext <extensions>`, `-f <extensions>` | Specify file extension(s) to process (comma-separated, overrides config file setting); defaults to `html,htm,shtml,shtm`; use `*` for all files | `--file-ext=html,php`, `--file-ext='*'` |
42
42
  | `--preset <name>`, `-p <name>` | Use a preset configuration (conservative or comprehensive) | `--preset=conservative` |
43
- | `--config-file <file>`, `-c <file>` | Use a configuration file (defaults to `html-minifier-next.config.json` in the working directory, if present) | `--config-file=path/to/config.json` |
43
+ | `--config-file <file>`, `-c <file>` | Use a configuration file (defaults to html-minifier-next.config.json in the working directory, if present) | `--config-file=path/to/config.json` |
44
44
  | `--verbose`, `-v` | Show detailed processing information (active options, file statistics) | `npx html-minifier-next --input-dir=src --output-dir=dist --verbose --collapse-whitespace` |
45
45
  | `--dry`, `-d` | Dry run: Process and report statistics without writing output | `npx html-minifier-next input.html --dry --collapse-whitespace` |
46
46
 
47
47
  ### Configuration file
48
48
 
49
- You can use a configuration file to specify options. If an `html-minifier-next.config.json` file is present in the current working directory—only there; subfolders (including `--input-dir`) and parent folders are not searched—the CLI picks it up automatically, with a note on STDERR confirming this. An explicit `--config-file` takes precedence. (The standalone `--zero` mode is deliberately config-free and ignores default config files.) The file can be either in JSON format or a JavaScript module that exports the configuration object:
49
+ You can use a configuration file to specify options. If an html-minifier-next.config.json file is present in the current working directory—only there; subfolders (including `--input-dir`) and parent folders are not searched—the CLI picks it up automatically, with a note on STDERR confirming this. An explicit `--config-file` takes precedence. (The standalone `--zero` mode is deliberately config-free and ignores default config files.) The file can be either in JSON format or a JavaScript module that exports the configuration object:
50
50
 
51
51
  **JSON configuration example:**
52
52
 
@@ -645,7 +645,7 @@ Parameters:
645
645
  * `--save`: Saves the run as the baseline (e.g., on `main` before switching to a branch)
646
646
  * `--core`: Disables the external minifiers (CSS, JS, SVG, URLs) to isolate HMN’s own processing time
647
647
  * `--iterations=N`: Sets the number of timed iterations (default 5; the median is reported)
648
- * `--config=PATH`: Uses an alternative options file (default `html-minifier-next.config.json`)
648
+ * `--config=PATH`: Uses an alternative options file (default html-minifier-next.config.json)
649
649
 
650
650
  To compare branches (A/B run), execute `npm run benchmark -- --save` on `main`, then `npm run benchmark` on the branch to see the deltas. Add `--core` on both ends when measuring changes to HMN’s own code rather than the bundled minifiers.
651
651
 
package/cli.js CHANGED
@@ -535,7 +535,7 @@ program.helpOption('-h, --help', 'Display help for command');
535
535
  }
536
536
 
537
537
  /**
538
- * Parse comma-separated ignore patterns into an array
538
+ * Parse comma-separated ignore patterns into an array.
539
539
  * @param {string} patterns - Comma-separated directory patterns (e.g., "libs,vendor")
540
540
  * @returns {string[]} Array of trimmed pattern strings with normalized separators
541
541
  */
@@ -548,8 +548,8 @@ program.helpOption('-h, --help', 'Display help for command');
548
548
  }
549
549
 
550
550
  /**
551
- * Check if a directory should be ignored based on ignore patterns
552
- * Supports matching by directory name or relative path
551
+ * Check if a directory should be ignored based on ignore patterns.
552
+ * Supports matching by directory name or relative path.
553
553
  * @param {string} dirPath - Absolute path to the directory
554
554
  * @param {string[]} ignorePatterns - Array of patterns to match against (with forward slashes)
555
555
  * @param {string} baseDir - Base directory for relative path calculation
@@ -791,9 +791,8 @@ program.helpOption('-h, --help', 'Display help for command');
791
791
  }
792
792
 
793
793
  // Prevent traversing into the output directory when it is inside the input directory
794
- let inputReal;
795
794
  let outputReal;
796
- inputReal = await fs.promises.realpath(inputDir).catch(() => undefined);
795
+ const inputReal = await fs.promises.realpath(inputDir).catch(() => undefined);
797
796
  try {
798
797
  outputReal = await fs.promises.realpath(outputDir);
799
798
  } catch {
@@ -7,7 +7,7 @@
7
7
  * - Otherwise: normalized absolute URL
8
8
  */
9
9
  /**
10
- * Create a URL minifier function for the given site context
10
+ * Create a URL minifier function for the given site context.
11
11
  * @param {string} site - The site base URL (used to compute relative URLs)
12
12
  * @returns {(url: string) => string} Minifier function that returns the shortest URL
13
13
  */
@@ -42,7 +42,7 @@ declare function isThenable(value: unknown): value is PromiseLike<any>;
42
42
  /** @param {string} value */
43
43
  declare function lowercase(value: string): string;
44
44
  /**
45
- * Asynchronously replace matches in a string
45
+ * Asynchronously replace matches in a string.
46
46
  * @param {string} str - Input string
47
47
  * @param {RegExp} regex - Regular expression with global flag
48
48
  * @param {Function} asyncFn - Async function to process each match
@@ -36,7 +36,7 @@ export declare const presets: {
36
36
  };
37
37
  };
38
38
  /**
39
- * Get preset configuration by name
39
+ * Get preset configuration by name.
40
40
  * @param {string} name - Preset name (“conservative” or “comprehensive”)
41
41
  * @returns {object|null} Preset options object or null if not found
42
42
  */
package/package.json CHANGED
@@ -10,17 +10,18 @@
10
10
  "entities": "^8.0.0",
11
11
  "lightningcss": "^1.33.0",
12
12
  "svgo": "^4.0.2",
13
- "terser": "^5.49.0"
13
+ "terser": "^5.50.0"
14
14
  },
15
15
  "description": "Highly effective, super-configurable, well-tested web page minifier (enhanced successor to HTML Minifier)",
16
16
  "devDependencies": {
17
- "@commitlint/cli": "^21.2.1",
17
+ "@commitlint/cli": "^21.2.2",
18
18
  "@eslint/js": "^10.0.1",
19
- "@swc/core": "^1.15.46",
20
- "@types/node": "^26.1.1",
21
- "eslint": "^10.8.0",
19
+ "@swc/core": "^1.15.47",
20
+ "@types/node": "^26.2.0",
21
+ "eslint": "^10.8.1",
22
+ "globals": "^17.11.0",
22
23
  "typescript": "^7.0.2",
23
- "vite": "^8.1.5"
24
+ "vite": "^8.2.1"
24
25
  },
25
26
  "engines": {
26
27
  "node": ">=22.13"
@@ -88,11 +89,11 @@
88
89
  "prepack": "npm run build",
89
90
  "prepare": "git config core.hooksPath .githooks || true",
90
91
  "serve": "vite",
91
- "test": "npm run test:types && node --test tests/*.spec.js",
92
+ "test": "npm run test:types && node --test \"test/**/*.test.js\"",
92
93
  "test:types": "tsc --noEmit && tsc --project tsconfig.test.json",
93
- "test:watch": "node --test --watch tests/*.spec.js"
94
+ "test:watch": "node --test --watch \"test/**/*.test.js\""
94
95
  },
95
96
  "type": "module",
96
97
  "types": "dist/types/htmlminifier.d.ts",
97
- "version": "7.5.2"
98
+ "version": "7.5.3"
98
99
  }
@@ -910,23 +910,23 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
910
910
  // Memoize `sortClassNames` results—class lists often repeat in templates
911
911
  const classNameCache = new LRU(500);
912
912
 
913
- options.sortClassNames = function (/** @type {string} */ value) {
913
+ options.sortClassNames = function (/** @type {string} */ classNames) {
914
914
  // Fast path: Single class (no spaces) needs no sorting
915
- if (value.indexOf(' ') === -1) {
916
- return value;
915
+ if (classNames.indexOf(' ') === -1) {
916
+ return classNames;
917
917
  }
918
918
 
919
919
  // Check cache first
920
- const cached = /** @type {string | undefined} */ (classNameCache.get(value));
920
+ const cached = /** @type {string | undefined} */ (classNameCache.get(classNames));
921
921
  if (cached !== undefined) {
922
922
  return cached;
923
923
  }
924
924
 
925
925
  // Expand UID tokens back to original content before sorting
926
926
  // Fast path: Skip if no HTML comments (UID markers) present
927
- let expandedValue = value;
928
- if (uidReplacePattern && value.indexOf('<!--') !== -1) {
929
- expandedValue = value.replace(uidReplacePattern, function (/** @type {string} */ _match, /** @type {string} */ index) {
927
+ let expandedValue = classNames;
928
+ if (uidReplacePattern && classNames.indexOf('<!--') !== -1) {
929
+ expandedValue = classNames.replace(uidReplacePattern, function (/** @type {string} */ _match, /** @type {string} */ index) {
930
930
  return ignoredMarkupChunks[+index] || '';
931
931
  });
932
932
  // Reset `lastIndex` for pattern reuse
@@ -939,7 +939,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
939
939
  const result = sorted.join(' ');
940
940
 
941
941
  // Cache the result
942
- classNameCache.set(value, result);
942
+ classNameCache.set(classNames, result);
943
943
  return result;
944
944
  };
945
945
  }
@@ -1148,11 +1148,11 @@ async function minifyHTML(value, options, partialMarkup) {
1148
1148
 
1149
1149
  // Look for trailing whitespaces, bypass any inline tags
1150
1150
  function trimTrailingWhitespace(/** @type {number} */ index, /** @type {string} */ nextTag) {
1151
- for (let endTag = ''; index >= 0 && canTrimWhitespace(endTag, emptyAttrs); index--) {
1151
+ for (let prevTag = ''; index >= 0 && canTrimWhitespace(prevTag, emptyAttrs); index--) {
1152
1152
  const str = buffer[index] ?? '';
1153
1153
  const match = str.match(/^<\/([\w:-]+)>$/);
1154
1154
  if (match) {
1155
- endTag = match[1] ?? '';
1155
+ prevTag = match[1] ?? '';
1156
1156
  } else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, '', nextTag, emptyAttrs, emptyAttrs, options, inlineElements, inlineTextSet))) {
1157
1157
  break;
1158
1158
  }
@@ -1398,13 +1398,13 @@ async function minifyHTML(value, options, partialMarkup) {
1398
1398
  // Collapse if both sides are element/closing tags or HTML comments, and neither is inline
1399
1399
  if ((currentTagMatch || currentIsHtmlComment || currentClosingTagMatch) &&
1400
1400
  (prevTagMatch || prevIsHtmlComment || prevClosingTagMatch)) {
1401
- const currentTag = currentTagMatch ? resolveName(currentTagMatch[1] ?? '')
1401
+ const currentTagName = currentTagMatch ? resolveName(currentTagMatch[1] ?? '')
1402
1402
  : currentClosingTagMatch ? resolveName(currentClosingTagMatch[1] ?? '') : null;
1403
- const prevTag = prevTagMatch ? resolveName(prevTagMatch[1] ?? '')
1403
+ const prevTagName = prevTagMatch ? resolveName(prevTagMatch[1] ?? '')
1404
1404
  : prevClosingTagMatch ? resolveName(prevClosingTagMatch[1] ?? '') : null;
1405
1405
 
1406
1406
  // Don’t collapse between inline elements (HTML comments count as non-inline)
1407
- if (!inlineElements.has(currentTag ?? '') && !inlineElements.has(prevTag ?? '')) {
1407
+ if (!inlineElements.has(currentTagName ?? '') && !inlineElements.has(prevTagName ?? '')) {
1408
1408
  // Collapse whitespace respecting context rules
1409
1409
  let collapsedText = prevText;
1410
1410
 
package/src/htmlparser.js CHANGED
@@ -154,7 +154,7 @@ function buildAttrRegex(handler) {
154
154
 
155
155
  /** @param {HTMLParserHandler} handler */
156
156
  function getAttrRegexForHandler(handler) {
157
- let cached = attrRegexCache.get(handler);
157
+ const cached = attrRegexCache.get(handler);
158
158
  if (cached) return cached;
159
159
  const compiled = buildAttrRegex(handler);
160
160
  attrRegexCache.set(handler, compiled);
@@ -166,7 +166,7 @@ const attrRegexStickyCache = new WeakMap();
166
166
 
167
167
  /** @param {HTMLParserHandler} handler */
168
168
  function getAttrRegexStickyForHandler(handler) {
169
- let cached = attrRegexStickyCache.get(handler);
169
+ const cached = attrRegexStickyCache.get(handler);
170
170
  if (cached) return cached;
171
171
  const nonSticky = getAttrRegexForHandler(handler);
172
172
  // Derive sticky version: Remove `^` anchor, add `y` flag
@@ -657,12 +657,12 @@ export class HTMLParser {
657
657
 
658
658
  // `needle` must already be lowercase
659
659
  function findTagInCurrentTable(/** @type {string} */ needle) {
660
- let pos;
661
- for (pos = stack.length - 1; pos >= 0; pos--) {
662
- const entry = stack[pos];
660
+ let stackIndex;
661
+ for (stackIndex = stack.length - 1; stackIndex >= 0; stackIndex--) {
662
+ const entry = stack[stackIndex];
663
663
  const currentTag = entry?.lowerTag;
664
664
  if (currentTag === needle) {
665
- return pos;
665
+ return stackIndex;
666
666
  }
667
667
  // Stop searching if hitting a table boundary
668
668
  if (currentTag === 'table') {
@@ -672,25 +672,25 @@ export class HTMLParser {
672
672
  return -1;
673
673
  }
674
674
 
675
- function parseEndTagAt(/** @type {number} */ pos) {
676
- // Close all open elements up to `pos` (mirrors `parseEndTag`’s core branch);
675
+ function parseEndTagAt(/** @type {number} */ stackIndex) {
676
+ // Close all open elements up to `stackIndex` (mirrors `parseEndTag`’s core branch);
677
677
  // `end` handlers are synchronous—invoked without awaiting, as in `parseEndTag`
678
- for (let i = stack.length - 1; i >= pos; i--) {
678
+ for (let i = stack.length - 1; i >= stackIndex; i--) {
679
679
  const entry = /** @type {NonNullable<(typeof stack)[number]>} */ (stack[i]);
680
680
  if (handler.end) {
681
681
  handler.end(entry.tag, entry.attrs, true);
682
682
  }
683
683
  }
684
- stack.length = pos;
685
- lastTag = pos ? (stack[pos - 1]?.tag ?? '') : '';
686
- lastTagLower = pos ? (stack[pos - 1]?.lowerTag ?? '') : '';
684
+ stack.length = stackIndex;
685
+ lastTag = stackIndex ? (stack[stackIndex - 1]?.tag ?? '') : '';
686
+ lastTagLower = stackIndex ? (stack[stackIndex - 1]?.lowerTag ?? '') : '';
687
687
  }
688
688
 
689
689
  function closeIfFoundInCurrentTable(/** @type {string} */ tagName) {
690
- const pos = findTagInCurrentTable(tagName);
691
- if (pos >= 0) {
690
+ const stackIndex = findTagInCurrentTable(tagName);
691
+ if (stackIndex >= 0) {
692
692
  // Close at the specific index to avoid re-searching
693
- parseEndTagAt(pos);
693
+ parseEndTagAt(stackIndex);
694
694
  return true;
695
695
  }
696
696
  return false;
@@ -812,38 +812,38 @@ export class HTMLParser {
812
812
 
813
813
  // `needle` must already be lowercase
814
814
  function findTag(/** @type {string} */ needle) {
815
- let pos;
816
- for (pos = stack.length - 1; pos >= 0; pos--) {
817
- if (stack[pos]?.lowerTag === needle) {
815
+ let stackIndex;
816
+ for (stackIndex = stack.length - 1; stackIndex >= 0; stackIndex--) {
817
+ if (stack[stackIndex]?.lowerTag === needle) {
818
818
  break;
819
819
  }
820
820
  }
821
- return pos;
821
+ return stackIndex;
822
822
  }
823
823
 
824
824
  async function parseEndTag(/** @type {string} */ tag, /** @type {string} */ tagName) {
825
- let pos;
825
+ let stackIndex;
826
826
  const lowerTagName = tagName ? tagName.toLowerCase() : '';
827
827
 
828
828
  // Find the closest opened tag of the same type
829
829
  if (tagName) {
830
- pos = findTag(lowerTagName);
830
+ stackIndex = findTag(lowerTagName);
831
831
  } else { // If no tag name is provided, clean shop
832
- pos = 0;
832
+ stackIndex = 0;
833
833
  }
834
834
 
835
- if (pos >= 0) {
835
+ if (stackIndex >= 0) {
836
836
  // Close all the open elements, up the stack
837
- for (let i = stack.length - 1; i >= pos; i--) {
837
+ for (let i = stack.length - 1; i >= stackIndex; i--) {
838
838
  if (handler.end) {
839
- handler.end(stack[i]?.tag, stack[i]?.attrs, i > pos || !tag);
839
+ handler.end(stack[i]?.tag, stack[i]?.attrs, i > stackIndex || !tag);
840
840
  }
841
841
  }
842
842
 
843
843
  // Remove the open elements from the stack
844
- stack.length = pos;
845
- lastTag = pos ? (stack[pos - 1]?.tag ?? '') : '';
846
- lastTagLower = pos ? (stack[pos - 1]?.lowerTag ?? '') : '';
844
+ stack.length = stackIndex;
845
+ lastTag = stackIndex ? (stack[stackIndex - 1]?.tag ?? '') : '';
846
+ lastTagLower = stackIndex ? (stack[stackIndex - 1]?.lowerTag ?? '') : '';
847
847
  } else if (handler.partialMarkup && tagName) {
848
848
  // In partial markup mode, preserve stray end tags
849
849
  if (handler.end) {
@@ -487,7 +487,7 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
487
487
  }
488
488
  // Sync path (`minifyCSS` disabled—identity function)
489
489
  if (cssResult && /^(?:[a-z-]+:[;\s]*)+$/i.test(cssResult)) return '';
490
- return cssResult != null ? cssResult : attrValue;
490
+ return cssResult ?? attrValue;
491
491
  }
492
492
  return attrValue;
493
493
  }
@@ -589,7 +589,7 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
589
589
  return originalAttrValue;
590
590
  });
591
591
  }
592
- return cssResult != null ? cssResult : attrValue;
592
+ return cssResult ?? attrValue;
593
593
  }
594
594
 
595
595
  if (tag === 'iframe' && attrName === 'srcdoc') {
@@ -635,7 +635,7 @@ function chooseAttributeQuote(attrValue, options) {
635
635
  */
636
636
  function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
637
637
  const attrName = options.name(attr.name);
638
- let attrValue = attr.value;
638
+ const attrValue = attr.value;
639
639
 
640
640
  // Entity decoding requires a lazy import—async only when `&` is present
641
641
  if (options.decodeEntities && attrValue && attrValue.indexOf('&') !== -1) {
package/src/lib/urls.js CHANGED
@@ -12,7 +12,7 @@ const REJECTED_SCHEMES = new Set(['data:', 'javascript:', 'mailto:']);
12
12
  const DIRECTORY_INDEXES = ['index.html', 'index.htm'];
13
13
 
14
14
  /**
15
- * Get the directory portion of a pathname (up to and including the last `/`)
15
+ * Get the directory portion of a pathname (up to and including the last `/`).
16
16
  * @param {string} pathname
17
17
  * @returns {string}
18
18
  */
@@ -22,7 +22,7 @@ function getDirectory(pathname) {
22
22
  }
23
23
 
24
24
  /**
25
- * Compute a path-relative URL from a base directory to a target path
25
+ * Compute a path-relative URL from a base directory to a target path.
26
26
  * @param {string} baseDir - Base directory path (must end with `/`)
27
27
  * @param {string} targetPath - Target pathname
28
28
  * @returns {string}
@@ -63,7 +63,7 @@ function relativize(baseDir, targetPath) {
63
63
  }
64
64
 
65
65
  /**
66
- * Remove directory index from the end of a pathname
66
+ * Remove directory index from the end of a pathname.
67
67
  * @param {string} pathname
68
68
  * @returns {string}
69
69
  */
@@ -77,7 +77,7 @@ function removeDirectoryIndex(pathname) {
77
77
  }
78
78
 
79
79
  /**
80
- * Create a URL minifier function for the given site context
80
+ * Create a URL minifier function for the given site context.
81
81
  * @param {string} site - The site base URL (used to compute relative URLs)
82
82
  * @returns {(url: string) => string} Minifier function that returns the shortest URL
83
83
  */
package/src/lib/utils.js CHANGED
@@ -109,7 +109,7 @@ function lowercase(value) {
109
109
  // Replace async helper
110
110
 
111
111
  /**
112
- * Asynchronously replace matches in a string
112
+ * Asynchronously replace matches in a string.
113
113
  * @param {string} str - Input string
114
114
  * @param {RegExp} regex - Regular expression with global flag
115
115
  * @param {Function} asyncFn - Async function to process each match
package/src/presets.js CHANGED
@@ -38,7 +38,7 @@ export const presets = {
38
38
  };
39
39
 
40
40
  /**
41
- * Get preset configuration by name
41
+ * Get preset configuration by name.
42
42
  * @param {string} name - Preset name (“conservative” or “comprehensive”)
43
43
  * @returns {object|null} Preset options object or null if not found
44
44
  */