html-minifier-next 7.6.0 → 8.1.0
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 +31 -21
- package/cli.js +152 -50
- package/dist/types/htmlminifier.d.ts +6 -4
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/lib/attributes.d.ts +2 -2
- package/dist/types/lib/attributes.d.ts.map +1 -1
- package/dist/types/lib/constants.d.ts +2 -2
- package/dist/types/lib/constants.d.ts.map +1 -1
- package/dist/types/lib/fragments.d.ts +46 -0
- package/dist/types/lib/fragments.d.ts.map +1 -0
- package/dist/types/lib/option-definitions.d.ts +21 -198
- package/dist/types/lib/option-definitions.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts +1 -1
- package/dist/types/lib/options.d.ts.map +1 -1
- package/dist/types/lib/unused-css.d.ts +5 -4
- package/dist/types/lib/unused-css.d.ts.map +1 -1
- package/dist/types/lib/utils.d.ts +34 -1
- package/dist/types/lib/utils.d.ts.map +1 -1
- package/dist/types/tokenchain.d.ts +2 -2
- package/dist/types/tokenchain.d.ts.map +1 -1
- package/html-minifier-next.schema.json +4 -5
- package/package.json +1 -1
- package/src/htmlminifier.js +49 -64
- package/src/htmlparser.js +4 -4
- package/src/lib/attributes.js +63 -6
- package/src/lib/constants.js +6 -8
- package/src/lib/fragments.js +274 -0
- package/src/lib/option-definitions.js +12 -4
- package/src/lib/options.js +44 -17
- package/src/lib/unused-css.js +5 -22
- package/src/lib/utils.js +671 -3
- package/src/tokenchain.js +37 -30
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ For editor support (validation, autocomplete, and inline documentation) in JSON
|
|
|
69
69
|
}
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
(If HMN is installed locally, you can also use the path
|
|
72
|
+
(If HMN is installed locally, you can also use the path ./node_modules/html-minifier-next/html-minifier-next.schema.json instead of the URL.)
|
|
73
73
|
|
|
74
74
|
**JavaScript module configuration example** (requires `"type": "module"` in the project’s package.json, or use a .mjs extension):
|
|
75
75
|
|
|
@@ -144,7 +144,6 @@ Options can be used in config files (camelCase) or via CLI flags (kebab-case wit
|
|
|
144
144
|
| `customAttrCollapse`<br>`--custom-attr-collapse` | Regex that specifies custom attribute to strip newlines from (e.g., `/ng-class/`) | `undefined` |
|
|
145
145
|
| `customAttrSurround`<br>`--custom-attr-surround` | Array of regexes that allow to support custom attribute surround expressions (e.g., `<input {{#if value}}checked="checked"{{/if}}>`) | `[]` |
|
|
146
146
|
| `customEventAttributes`<br>`--custom-event-attributes` | Array of regexes that allow to support custom event attributes for `minifyJS` (e.g., `ng-click`) | `[ /^on[a-z]{3,}$/ ]` |
|
|
147
|
-
| `customFragmentQuantifierLimit`<br>`--custom-fragment-quantifier-limit` | Set maximum quantifier limit for custom fragments to prevent ReDoS attacks | `200` |
|
|
148
147
|
| `decodeEntities`<br>`--decode-entities` | Use direct Unicode characters whenever possible | `false` |
|
|
149
148
|
| `ignoreCustomComments`<br>`--ignore-custom-comments` | Array of regexes that allow to ignore matching comments | `[ /^!/, /^\s*#/ ]` |
|
|
150
149
|
| `ignoreCustomFragments`<br>`--ignore-custom-fragments` | Array of regexes that allow to ignore certain fragments, when matched (e.g., `<?php … ?>`, `{{ … }}`, etc.) | `[ /<%[\s\S]*?%>/, /<\?[\s\S]*?\?>/ ]` |
|
|
@@ -176,6 +175,7 @@ Options can be used in config files (camelCase) or via CLI flags (kebab-case wit
|
|
|
176
175
|
| `removeUnusedCSS`<br>`--remove-unused-css` | [Remove unused CSS rules](#unused-css-removal) from `style` elements; requires `minifyCSS`; **note that this can change how a document renders** | `false` (could be `true`, `{ safelist, scripts }`) |
|
|
177
176
|
| `sortAttributes`<br>`--sort-attributes` | [Sort attributes by frequency](#sorting-attributes-and-style-classes) | `false` |
|
|
178
177
|
| `sortClassNames`<br>`--sort-class-names` | [Sort style classes by frequency](#sorting-attributes-and-style-classes) | `false` |
|
|
178
|
+
| `strictCustomFragments`<br>`--strict-custom-fragments` | [Reject `ignoreCustomFragments` patterns that risk catastrophic backtracking](#redos-protection) (rather than warning about them) | `false` |
|
|
179
179
|
| `trimCustomFragments`<br>`--trim-custom-fragments` | Trim whitespace around custom fragments (`ignoreCustomFragments`) | `false` |
|
|
180
180
|
| `useShortDoctype`<br>`--use-short-doctype` | [Replaces the doctype with the short HTML doctype](https://perfectionkills.com/experimenting-with-html-minifier/#use_short_doctype) | `false` |
|
|
181
181
|
|
|
@@ -253,7 +253,7 @@ Symbols are considered used when they appear
|
|
|
253
253
|
|
|
254
254
|
Names carrying characters that end a CSS identifier—`md:flex`, `w-1/2`, `p-[3px]`—are matched as whole tokens, so utility-CSS class names survive whether they come from markup, a `data-*` value, or a string in an inline script.
|
|
255
255
|
|
|
256
|
-
**Class names that only appear in external scripts cannot be detected.** A minifier sees one document, not the DOM that scripts later build from it, so a class added by
|
|
256
|
+
**Class names that only appear in external scripts cannot be detected.** A minifier sees one document, not the DOM that scripts later build from it, so a class added by bundle.js looks exactly like a class nobody uses. List those under `safelist`, as strings or regular expressions:
|
|
257
257
|
|
|
258
258
|
```js
|
|
259
259
|
const result = await minify(html, {
|
|
@@ -539,7 +539,7 @@ SVG and MathML elements are automatically recognized as foreign elements, and wh
|
|
|
539
539
|
|
|
540
540
|
### Working with invalid or partial markup
|
|
541
541
|
|
|
542
|
-
By default,
|
|
542
|
+
By default, HMN parses markup into a complete tree structure, then modifies it (removing anything that was specified for removal, ignoring anything that was specified to be ignored, etc.), then creates markup from that tree and returns it.
|
|
543
543
|
|
|
544
544
|
_Input markup (e.g., `<p id="">foo`) → Internal representation of markup in a form of tree (e.g., `{ tag: "p", attr: "id", children: ["foo"] }`) → Transformation of internal representation (e.g., removal of `id` attribute) → Output of resulting markup (e.g., `<p>foo</p>`)_
|
|
545
545
|
|
|
@@ -547,40 +547,52 @@ For partial HTML fragments (such as template includes, SSI fragments, or closing
|
|
|
547
547
|
|
|
548
548
|
To validate complete HTML markup, use [the W3C validator](https://validator.w3.org/) or one of [several validator packages](https://meiert.com/blog/html-validator-packages/).
|
|
549
549
|
|
|
550
|
+
### Regex options and flags
|
|
551
|
+
|
|
552
|
+
`customAttrAssign` and `customAttrSurround` patterns are merged into one attribute pattern which carries no flags of its own. `i` and `s` are written into each pattern’s source instead, so they survive the merge. `u`, `v`, and `m` cannot be, and none of them fails loudly when dropped: `u` and `v` only narrow what syntax is legal, so a source valid under either stays valid without it and quietly matches something else—a dropped `u` leaves `\p{L}` matching the literal text `p{L}`—while a dropped `m` leaves `^` and `$` matching at the ends of the input rather than of each line.
|
|
553
|
+
|
|
554
|
+
A pattern is therefore refused with an error where the flag changes what its source matches—a property or code point escape, a character past the BMP, a character `i` folds by Unicode rules only while `u` is there (`/s/iu` matches `\u017F`, `/k/iu` matches `\u212A`), a `v` class that nests, subtracts, intersects, or holds strings, or—under `m`—an anchor whose meaning moves. A flag the source does not depend on, as in `/x=/u`, is left alone.
|
|
555
|
+
|
|
556
|
+
Patterns given as strings, in a configuration file or on the command line, may be written either bare (`ng-class`) or delimited with flags (`/ng-class/i`).
|
|
557
|
+
|
|
550
558
|
## Security
|
|
551
559
|
|
|
552
560
|
### ReDoS protection
|
|
553
561
|
|
|
554
|
-
|
|
562
|
+
You can use `ignoreCustomFragments` to hand HTML Minifier Next a regular expression to run against your documents. This is also where a regular expression denial of service (ReDoS) could originate:
|
|
555
563
|
|
|
556
|
-
*
|
|
564
|
+
* Matching without backtracking: A pattern that wraps an any-character or negated-class body in literal delimiters—`<%[\s\S]*?%>` or `\{\{[^}]*?\}\}`, and every other shape below—is matched by scanning for those delimiters in linear time, with no regular expression involved. Patterns of other shapes run as regular expressions, one per pattern, so each keeps its own flags.
|
|
557
565
|
|
|
558
|
-
*
|
|
566
|
+
* Pattern detection: HMN warns about the shapes that backtrack catastrophically—an unlimited quantifier over a group that itself contains a quantifier that can vary (`(a+)+`, `(a?)+`) or alternation (`(a|b)*`), and two unbounded repeats that can consume the same character with only atoms matching empty between them (`.*.*`, `[a]*a*`, `\w*\d*`, `a*b*a*`).
|
|
559
567
|
|
|
560
|
-
|
|
568
|
+
A group is no wall: It counts by what its body can match, and repeats meet across its boundary, so `\s*(\w*)\s*` and `(a*)a*` are flagged like `\s*\w*\s*` and `a*a*`. A lookaround backtracks nothing, so `(?=a*)a*` passes. A fixed count does not vary, so `(?:a{4})+` passes; repeats that share no character leave nothing ambiguous to split, so `\s*\S*` passes. A pattern is read the way its own flags make it match, so `/.*\n*/s` and `/[a]*A*/i` are flagged where those same sources without the flags are not. Under `v`, a class that nests reads as the union it is, while one that subtracts (`--`) or intersects (`&&`) is left unread and passes.
|
|
561
569
|
|
|
562
|
-
|
|
570
|
+
These are also shapes a linear scan cannot stand in for. `strictCustomFragments` refuses them with an error instead, which is worth enabling where the patterns or the input are not entirely under your control. A pattern longer than 10,000 characters or nested more than 50 groups deep is judged risky without being analyzed further, so that reading the pattern cannot itself become the expensive step. The check reads shapes, not languages: It warns about the common ones rather than proving a pattern linear, and misses repeats that overlap only across whole subexpressions (`(ab)*(abab)*`). **Treat a pattern that passes as unflagged, not as vetted.**
|
|
571
|
+
|
|
572
|
+
* Input length limits: The `maxInputLength` option allows you to set a maximum input size to prevent processing of excessively large inputs that could cause performance issues.
|
|
573
|
+
|
|
574
|
+
**Important:** A single unlimited quantifier is not one of those shapes: `[\s\S]*?` running up to a literal terminator matches in linear time, and it is how HMN’s defaults are written. Bounds are still worth adding where you know the maximum length of a fragment, since they cap how far a failing match can scan.
|
|
563
575
|
|
|
564
576
|
#### Custom fragment examples
|
|
565
577
|
|
|
566
|
-
**Safe patterns
|
|
578
|
+
**Safe patterns:**
|
|
567
579
|
|
|
568
580
|
```js
|
|
569
581
|
ignoreCustomFragments: [
|
|
570
|
-
/<%[\s\S]
|
|
571
|
-
/<\?php[\s\S]{0,5000}?\?>/, // PHP with bounds
|
|
582
|
+
/<%[\s\S]*?%>/, // Lazy scan up to a literal terminator
|
|
583
|
+
/<\?php[\s\S]{0,5000}?\?>/, // PHP with explicit bounds
|
|
572
584
|
/\{\{[^}]{0,500}\}\}/ // Handlebars without nested braces
|
|
573
585
|
]
|
|
574
586
|
```
|
|
575
587
|
|
|
576
|
-
**
|
|
588
|
+
**Unsafe patterns** (these trigger warnings):
|
|
577
589
|
|
|
578
590
|
```js
|
|
579
591
|
ignoreCustomFragments: [
|
|
580
|
-
/<%
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
592
|
+
/<%(\s|\S)*?%>/, // Unlimited quantifier over an alternating group
|
|
593
|
+
/\{\{([^}]+)+\}\}/, // Nested unlimited quantifiers
|
|
594
|
+
/<!--[\s\S]*[\s\S]*-->/, // Two unbounded repeats in a row
|
|
595
|
+
/<%\w*\d*%>/ // Two unbounded repeats over overlapping sets
|
|
584
596
|
]
|
|
585
597
|
```
|
|
586
598
|
|
|
@@ -600,8 +612,6 @@ ignoreCustomFragments: [/\{\{[\s\S]{0,500}?\}\}/]
|
|
|
600
612
|
ignoreCustomFragments: [/\{\{[\s\S]{0,500}?\}\}/]
|
|
601
613
|
```
|
|
602
614
|
|
|
603
|
-
**Important:** When using custom `ignoreCustomFragments`, the minifier automatically applies bounded quantifiers to prevent ReDoS attacks, but you can also write safer patterns yourself using explicit bounds.
|
|
604
|
-
|
|
605
615
|
##### Escaping patterns in different contexts
|
|
606
616
|
|
|
607
617
|
The escaping requirements for `ignoreCustomFragments` patterns differ depending on how you’re using HMN:
|
|
@@ -682,11 +692,11 @@ Parameters:
|
|
|
682
692
|
|
|
683
693
|
* No argument: Runs and, if a baseline exists, shows size and time deltas
|
|
684
694
|
* `--save`: Saves the run as the baseline (e.g., on `main` before switching to a branch)
|
|
685
|
-
* `--core`: Disables the external minifiers (CSS, JS, SVG, URLs) to isolate HMN’s
|
|
695
|
+
* `--core`: Disables the external minifiers (CSS, JS, SVG, URLs) to isolate HMN’s processing time
|
|
686
696
|
* `--iterations=N`: Sets the number of timed iterations (default 5; the median is reported)
|
|
687
697
|
* `--config=PATH`: Uses an alternative options file (default html-minifier-next.config.json)
|
|
688
698
|
|
|
689
|
-
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
|
|
699
|
+
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 rather than bundled minifiers.
|
|
690
700
|
|
|
691
701
|
#### Profiling
|
|
692
702
|
|