html-minifier-next 8.2.0 → 8.2.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
@@ -94,6 +94,8 @@ const result = await minify('<p title="example" id="moo">foo</p>', {
94
94
  console.log(result); // `<p title=example id=moo>foo`
95
95
  ```
96
96
 
97
+ (CommonJS consumers can still use dynamic import—`const { minify } = await import('html-minifier-next')`—as `minify()` is async.)
98
+
97
99
  See [the original blog post](https://perfectionkills.com/experimenting-with-html-minifier/) for details of [how it works](https://perfectionkills.com/experimenting-with-html-minifier/#how_it_works), [descriptions of most options](https://perfectionkills.com/experimenting-with-html-minifier/#options), [testing results](https://perfectionkills.com/experimenting-with-html-minifier/#field_testing), and [conclusions](https://perfectionkills.com/experimenting-with-html-minifier/#cost_and_benefits).
98
100
 
99
101
  ## Presets
@@ -135,9 +137,9 @@ Options can be used in config files (camelCase) or via CLI flags (kebab-case wit
135
137
  | `caseSensitive`<br>`--case-sensitive` | Treat attributes in case-sensitive manner (useful for custom HTML elements) | `false` |
136
138
  | `collapseAttributeWhitespace`<br>`--collapse-attribute-whitespace` | Trim and collapse whitespace characters within attribute values | `false` |
137
139
  | `collapseBooleanAttributes`<br>`--collapse-boolean-attributes` | [Omit attribute values from boolean attributes](https://perfectionkills.com/experimenting-with-html-minifier/#collapse_boolean_attributes) | `false` |
138
- | `collapseInlineTagWhitespace`<br>`--collapse-inline-tag-whitespace` | Collapse whitespace more aggressively between inline elements—use with `collapseWhitespace: true` | `false` |
139
- | `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` |
140
- | `conservativeCollapse`<br>`--conservative-collapse` | Always collapse to one space (never remove it entirely)—use with `collapseWhitespace: true` | `false` |
140
+ | `collapseInlineTagWhitespace`<br>`--collapse-inline-tag-whitespace` | Collapse whitespace more aggressively between inline elements—use with [`collapseWhitespace`](#combining-whitespace-options) | `false` |
141
+ | `collapseWhitespace`<br>`--collapse-whitespace` | [Collapse whitespace that contributes to text nodes in a document tree](https://perfectionkills.com/experimenting-with-html-minifier/#collapse_whitespace); [enable other whitespace options](#combining-whitespace-options) | `false` |
142
+ | `conservativeCollapse`<br>`--conservative-collapse` | Always collapse to one space (never remove it entirely)—use with [`collapseWhitespace`](#combining-whitespace-options) | `false` |
141
143
  | `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` |
142
144
  | `continueOnParseError`<br>`--continue-on-parse-error` | [Handle parse errors](https://html.spec.whatwg.org/multipage/parsing.html#parse-errors) instead of aborting | `false` |
143
145
  | `customAttrAssign`<br>`--custom-attr-assign` | Array of regexes that allow to support custom attribute assign expressions (e.g., `<div flex?="{{mode != cover}}"></div>`) | `[]` |
@@ -159,7 +161,7 @@ Options can be used in config files (camelCase) or via CLI flags (kebab-case wit
159
161
  | `minifyURLs`<br>`--minify-urls` | Minify URLs in various attributes | `false` (could be `true`, `String`, `Object`, `Function(text)`) |
160
162
  | `noNewlinesBeforeTagClose`<br>`--no-newlines-before-tag-close` | Never add a newline before a tag that closes an element | `false` |
161
163
  | `partialMarkup`<br>`--partial-markup` | Treat input as a partial HTML fragment, preserving stray end tags (closing tags without opening tags) and preventing auto-closing of unclosed tags at end of input | `false` |
162
- | `preserveLineBreaks`<br>`--preserve-line-breaks` | Always collapse to one line break (never remove it entirely) when whitespace between tags includes a line break—use with `collapseWhitespace: true` | `false` |
164
+ | `preserveLineBreaks`<br>`--preserve-line-breaks` | Always collapse to one line break (never remove it entirely) when whitespace between tags includes a line break—use with [`collapseWhitespace`](#combining-whitespace-options) | `false` |
163
165
  | `preventAttributesEscaping`<br>`--prevent-attributes-escaping` | Prevents the escaping of the values of attributes | `false` |
164
166
  | `processScripts`<br>`--process-scripts` | Array of strings corresponding to types of `script` elements to process through minifier (e.g., `text/ng-template`, `text/x-handlebars-template`, etc.) | `[]` |
165
167
  | `quoteCharacter`<br>`--quote-character` | Type of quote to use for attribute values (`'` or `"`) | Auto-detected (uses the quote requiring less escaping; defaults to `"` when equal) |
@@ -172,7 +174,7 @@ Options can be used in config files (camelCase) or via CLI flags (kebab-case wit
172
174
  | `removeOptionalTags`<br>`--remove-optional-tags` | [Remove optional tags](https://perfectionkills.com/experimenting-with-html-minifier/#remove_optional_tags) | `false` |
173
175
  | `removeRedundantAttributes`<br>`--remove-redundant-attributes` | [Remove attributes when value matches default](https://meiert.com/blog/optional-html/#toc-attribute-values) | `false` |
174
176
  | `removeTagWhitespace`<br>`--remove-tag-whitespace` | Remove space between attributes whenever possible; **note that this will result in invalid HTML** | `false` |
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
+ | `removeUnusedCSS`<br>`--remove-unused-css` | [Remove unused CSS rules](#unused-css-removal) from `style` elements—use with `minifyCSS`; **note that this can change how a document renders** | `false` (could be `true`, `{ safelist, scripts }`) |
176
178
  | `sortAttributes`<br>`--sort-attributes` | [Sort attributes by frequency](#sorting-attributes-and-style-classes) | `false` |
177
179
  | `sortClassNames`<br>`--sort-class-names` | [Sort style classes by frequency](#sorting-attributes-and-style-classes) | `false` |
178
180
  | `strictCustomFragments`<br>`--strict-custom-fragments` | [Reject `ignoreCustomFragments` patterns that risk catastrophic backtracking](#redos-protection) (rather than warning about them) | `false` |
@@ -189,6 +191,31 @@ A few options take functions and are therefore only available programmatically,
189
191
  | `canTrimWhitespace` | `Function(tag, attrs, defaultFn)` that determines whether leading and trailing whitespace around an element may be trimmed | Built-in handling |
190
192
  | `log` | `Function(message)` called with warnings and errors, including minification errors swallowed by `continueOnMinifyError` (e.g., pass `console.error` to surface them); the CLI wires this up under `--verbose` and `--dry` | No-op (errors are silent) |
191
193
 
194
+ ### Combining whitespace options
195
+
196
+ `collapseInlineTagWhitespace`, `conservativeCollapse`, and `preserveLineBreaks` are modifiers: They do nothing on their own, and only take effect when `collapseWhitespace` is enabled.
197
+
198
+ Given input
199
+
200
+ ```html
201
+ <nav>
202
+ <button>A</button> <button>B</button>
203
+ </nav>
204
+ ```
205
+
206
+ you get the following output (condensed, `\n` represents an actual line break):
207
+
208
+ | Options | Output |
209
+ | --- | --- |
210
+ | `collapseInlineTagWhitespace` | `<nav>\n <button>A</button> <button>B</button>\n</nav>` (unchanged) |
211
+ | `collapseWhitespace` | `<nav><button>A</button> <button>B</button></nav>` |
212
+ | `collapseWhitespace`, `collapseInlineTagWhitespace` | `<nav><button>A</button><button>B</button></nav>` |
213
+ | `collapseWhitespace`, `conservativeCollapse` | `<nav> <button>A</button> <button>B</button> </nav>` |
214
+ | `collapseWhitespace`, `preserveLineBreaks` | `<nav>\n<button>A</button> <button>B</button>\n</nav>` |
215
+ | `collapseWhitespace`, `preserveLineBreaks`, `collapseInlineTagWhitespace` | `<nav>\n<button>A</button><button>B</button>\n</nav>` |
216
+
217
+ Where the modifiers disagree, the preserving one wins—`conservativeCollapse` and `preserveLineBreaks` do not let `collapseInlineTagWhitespace` remove a space or line break entirely.
218
+
192
219
  ### Sorting attributes and style classes
193
220
 
194
221
  Minifier options like `sortAttributes` and `sortClassNames` won’t impact the plain-text size of the output. However, using these options for more consistent ordering improves the compression ratio for Gzip and Brotli used over HTTP.
@@ -234,7 +261,7 @@ const result = await minify(html, {
234
261
 
235
262
  ### Unused CSS removal
236
263
 
237
- `removeUnusedCSS` removes rules from `style` elements whose class or ID selectors the document doesn’t reference. It requires `minifyCSS`, because the removal runs through Lightning CSS—passing `minifyCSS` a function of your own replaces that step, so the removal does not apply, either. Both cases are reported through [the `log` hook](#api-only-options). It does not touch `style` or `media` attributes.
264
+ `removeUnusedCSS` removes rules from `style` elements whose class or ID selectors the document doesn’t reference. It needs to be used with `minifyCSS`, because the removal runs through Lightning CSS—passing `minifyCSS` a function of your own replaces that step, so the removal does not apply, either. Both cases are reported through [the `log` hook](#api-only-options). It does not touch `style` or `media` attributes.
238
265
 
239
266
  ```js
240
267
  const result = await minify(html, {
@@ -1 +1 @@
1
- {"version":3,"file":"htmlparser.d.ts","sourceRoot":"","sources":["../../src/htmlparser.js"],"names":[],"mappings":"AAeG,YAYG,iBAAiB,GAZV;IACR,KAAK,CAAC,WAAW;IACjB,GAAG,CAAC,WAAW;IACf,KAAK,CAAC,WAAW;IACjB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,WAAW;IACnB,oBAAoB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3C,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACpC,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACnC,gBAAgB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,kBAAkB,CAAC,EAAE,MAAM,EAAE,EAAE,GAAG,SAAS,CAAC;IAC5C,gBAAgB,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;CACxC,CAAmB;AA0CvB,eAAO,MAAM,MAAM,QAA8C,CAAC;AAoJlE,qBAAa,UAAU;IAMd,IAAI;IACJ,OAAO;IANd;;;OAGG;IACH,YAAY,IAAI,EAHL,MAGK,EAAE,OAAO,EAFd,iBAEc,EAGxB;IAEK,KAAK,kBA4uBV;CACF"}
1
+ {"version":3,"file":"htmlparser.d.ts","sourceRoot":"","sources":["../../src/htmlparser.js"],"names":[],"mappings":"AAeG,YAYG,iBAAiB,GAZV;IACR,KAAK,CAAC,WAAW;IACjB,GAAG,CAAC,WAAW;IACf,KAAK,CAAC,WAAW;IACjB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,WAAW;IACnB,oBAAoB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3C,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACpC,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACnC,gBAAgB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,kBAAkB,CAAC,EAAE,MAAM,EAAE,EAAE,GAAG,SAAS,CAAC;IAC5C,gBAAgB,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;CACxC,CAAmB;AA0CvB,eAAO,MAAM,MAAM,QAA8C,CAAC;AA2JlE,qBAAa,UAAU;IAMd,IAAI;IACJ,OAAO;IANd;;;OAGG;IACH,YAAY,IAAI,EAHL,MAGK,EAAE,OAAO,EAFd,iBAEc,EAGxB;IAEK,KAAK,kBAkxBV;CACF"}
@@ -100,7 +100,8 @@ declare const trailingElements: Set<string>;
100
100
  declare const htmlElements: Set<string>;
101
101
  declare const specialContentElements: Set<string>;
102
102
  declare const genericRawTextElements: Set<string>;
103
+ declare const endlessRawTextElements: Set<string>;
103
104
  declare const escapableRawTextElements: Set<string>;
104
105
  declare const rawTextElements: Set<string>;
105
- export { RE_WS_START, RE_WS_END, RE_WS_ONLY, RE_WS_NBSP_END, RE_ALL_WS_NBSP, RE_NBSP_LEADING_GROUP, RE_NBSP_LEAD_GROUP, RE_NBSP_TRAILING_GROUP, RE_NBSP_TRAILING_STRIP, RE_EVENT_ATTR_DEFAULT, RE_CAN_REMOVE_ATTR_QUOTES, RE_TRAILING_SEMICOLON, RE_AMP_ENTITY, RE_LEGACY_ENTITIES, RE_ESCAPE_LT, RE_ESCAPE_LT_RAW_TEXT, RE_HTML_ENCODING, RE_ATTR_WS_CHECK, RE_ATTR_WS_COLLAPSE, RE_ATTR_WS_TRIM, RE_STYLE_ELEMENT, RE_EMPTY_ATTRIBUTE, inlineElementsToKeepWhitespaceAround, inlineElementsToKeepWhitespaceWithin, inlineElementsToKeepWhitespace, formControlElements, generalDefaults, tagDefaults, executableScriptsMimetypes, keepScriptsMimetypes, jsonScriptTypes, isSimpleBoolean, isBooleanValue, collapsibleValues, srcsetElements, optionalStartTags, optionalEndTags, headerElements, descriptionElements, pBlockElements, pInlineElements, rubyEndTagOmission, rubyRtcEndTagOmission, optionElements, tableContentElements, tableSectionElements, cellElements, topLevelElements, compactElements, looseElements, trailingElements, htmlElements, specialContentElements, genericRawTextElements, escapableRawTextElements, rawTextElements };
106
+ export { RE_WS_START, RE_WS_END, RE_WS_ONLY, RE_WS_NBSP_END, RE_ALL_WS_NBSP, RE_NBSP_LEADING_GROUP, RE_NBSP_LEAD_GROUP, RE_NBSP_TRAILING_GROUP, RE_NBSP_TRAILING_STRIP, RE_EVENT_ATTR_DEFAULT, RE_CAN_REMOVE_ATTR_QUOTES, RE_TRAILING_SEMICOLON, RE_AMP_ENTITY, RE_LEGACY_ENTITIES, RE_ESCAPE_LT, RE_ESCAPE_LT_RAW_TEXT, RE_HTML_ENCODING, RE_ATTR_WS_CHECK, RE_ATTR_WS_COLLAPSE, RE_ATTR_WS_TRIM, RE_STYLE_ELEMENT, RE_EMPTY_ATTRIBUTE, inlineElementsToKeepWhitespaceAround, inlineElementsToKeepWhitespaceWithin, inlineElementsToKeepWhitespace, formControlElements, generalDefaults, tagDefaults, executableScriptsMimetypes, keepScriptsMimetypes, jsonScriptTypes, isSimpleBoolean, isBooleanValue, collapsibleValues, srcsetElements, optionalStartTags, optionalEndTags, headerElements, descriptionElements, pBlockElements, pInlineElements, rubyEndTagOmission, rubyRtcEndTagOmission, optionElements, tableContentElements, tableSectionElements, cellElements, topLevelElements, compactElements, looseElements, trailingElements, htmlElements, specialContentElements, genericRawTextElements, endlessRawTextElements, escapableRawTextElements, rawTextElements };
106
107
  //# sourceMappingURL=constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/lib/constants.js"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/lib/constants.js"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"whitespace.d.ts","sourceRoot":"","sources":["../../../src/lib/whitespace.js"],"names":[],"mappings":"AAeA,0BAA0B;AAC1B,QAAA,MAAM,cAAc,QADR,MAAM,WAQjB,CAAC;AAIF,0BAA0B;AAC1B,iBAAS,qBAAqB,CAAC,GAAG,EADtB,MACsB,UAgBjC;AAID;;;;;;GAMG;AACH,iBAAS,kBAAkB,CAAC,GAAG,EANpB,MAMoB,EAAE,OAAO,EAL7B;IAAC,kBAAkB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAAC,oBAAoB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAKxD,EAAE,QAAQ,EAJvC,OAIuC,EAAE,SAAS,EAHlD,OAGkD,EAAE,WAAW,AAFvE,CAA8B,EAAtB,OAEuE,UAqFjF;AAoBD;;;;;;;;;GASG;AACH,iBAAS,uBAAuB,CAAC,GAAG,EATzB,MASyB,EAAE,OAAO,EARlC,MAQkC,EAAE,OAAO,EAP3C,MAO2C,EAAE,SAAS,EANtD,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAC,CAMkB,EAAE,SAAS,EALjE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAC,CAK6B,EAAE,OAAO,EAJ1E;IAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAAC,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAAC,2BAA2B,CAAC,EAAE,OAAO,CAAA;CAI1B,EAAE,cAAc,EAH1F,GAAG,CAAC,MAAM,CAGgF,EAAE,aAAa,EAFzG,GAAG,CAAC,MAAM,CAE+F,UAiEnH;AASD,0BAA0B;AAC1B,iBAAS,qBAAqB,CAAC,GAAG,EADtB,MACsB,WAEjC;AAED,0BAA0B;AAC1B,iBAAS,iBAAiB,CAAC,GAAG,EADlB,MACkB,WAE7B;AAID,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,kBAAkB,EAClB,uBAAuB,EACvB,qBAAqB,EACrB,iBAAiB,EAClB,CAAC"}
1
+ {"version":3,"file":"whitespace.d.ts","sourceRoot":"","sources":["../../../src/lib/whitespace.js"],"names":[],"mappings":"AAeA,0BAA0B;AAC1B,QAAA,MAAM,cAAc,QADR,MAAM,WAQjB,CAAC;AAIF,0BAA0B;AAC1B,iBAAS,qBAAqB,CAAC,GAAG,EADtB,MACsB,UAgBjC;AAID;;;;;;GAMG;AACH,iBAAS,kBAAkB,CAAC,GAAG,EANpB,MAMoB,EAAE,OAAO,EAL7B;IAAC,kBAAkB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAAC,oBAAoB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAKxD,EAAE,QAAQ,EAJvC,OAIuC,EAAE,SAAS,EAHlD,OAGkD,EAAE,WAAW,AAFvE,CAA8B,EAAtB,OAEuE,UAqFjF;AAoBD;;;;;;;;;GASG;AACH,iBAAS,uBAAuB,CAAC,GAAG,EATzB,MASyB,EAAE,OAAO,EARlC,MAQkC,EAAE,OAAO,EAP3C,MAO2C,EAAE,SAAS,EANtD,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAC,CAMkB,EAAE,SAAS,EALjE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAC,CAK6B,EAAE,OAAO,EAJ1E;IAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAAC,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAAC,2BAA2B,CAAC,EAAE,OAAO,CAAA;CAI1B,EAAE,cAAc,EAH1F,GAAG,CAAC,MAAM,CAGgF,EAAE,aAAa,EAFzG,GAAG,CAAC,MAAM,CAE+F,UA4EnH;AASD,0BAA0B;AAC1B,iBAAS,qBAAqB,CAAC,GAAG,EADtB,MACsB,WAEjC;AAED,0BAA0B;AAC1B,iBAAS,iBAAiB,CAAC,GAAG,EADlB,MACkB,WAE7B;AAID,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,kBAAkB,EAClB,uBAAuB,EACvB,qBAAqB,EACrB,iBAAiB,EAClB,CAAC"}
@@ -275,7 +275,7 @@
275
275
  "type": "boolean"
276
276
  },
277
277
  "removeUnusedCSS": {
278
- "description": "Remove rules from `style` elements whose class or ID selectors the document never references (requires `--minify-css`); note that class names only applied by external scripts cannot be detected—use `{\"safelist\": […]}` for those",
278
+ "description": "Remove rules from `style` elements whose class or ID selectors the document never references—use with `--minify-css`; note that class names only applied by external scripts cannot be detected—use `{\"safelist\": […]}` for those",
279
279
  "type": [
280
280
  "boolean",
281
281
  "object"
package/package.json CHANGED
@@ -95,5 +95,5 @@
95
95
  },
96
96
  "type": "module",
97
97
  "types": "dist/types/htmlminifier.d.ts",
98
- "version": "8.2.0"
98
+ "version": "8.2.2"
99
99
  }
package/src/htmlparser.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { isThenable, embedSource, findTagEnd } from './lib/utils.js';
9
- import { escapableRawTextElements, genericRawTextElements, RE_HTML_ENCODING } from './lib/constants.js';
9
+ import { endlessRawTextElements, escapableRawTextElements, genericRawTextElements, RE_HTML_ENCODING } from './lib/constants.js';
10
10
 
11
11
  /** @import { HTMLAttribute } from './lib/attributes.js' */
12
12
 
@@ -103,6 +103,13 @@ const nonPhrasing = new Set(['address', 'article', 'aside', 'base', 'blockquote'
103
103
  const endsTagName = (/** @type {string} */ character) =>
104
104
  character === '' || character === '/' || character === '>' || /\s/.test(character);
105
105
 
106
+ // The characters `\s` matches, by code: Tab through carriage return, space, and the
107
+ // Unicode whitespace and line terminators
108
+ const isWhitespaceCode = (/** @type {number} */ code) =>
109
+ (code >= 9 && code <= 13) || code === 32 || code === 0xA0 || code === 0x1680 ||
110
+ (code >= 0x2000 && code <= 0x200A) || code === 0x2028 || code === 0x2029 ||
111
+ code === 0x202F || code === 0x205F || code === 0x3000 || code === 0xFEFF;
112
+
106
113
  /**
107
114
  * @param {string} text - The input the tag stands in
108
115
  * @param {number} nameEnd - Position right after the tag name
@@ -251,8 +258,6 @@ export class HTMLParser {
251
258
 
252
259
  // Sticky regex versions for position-based matching (avoids string slicing)
253
260
  const startTagOpenY = new RegExp(startTagOpen.source.slice(1), 'y');
254
- // `\s*` with sticky flag is O(n) at worst—no retry from different positions possible
255
- const startTagCloseY = /\s*(\/?)>/y;
256
261
  const endTagOpenY = new RegExp(endTagOpen.source.slice(1), 'y');
257
262
  const doctypeY = /<!DOCTYPE[^<>]+>/iy;
258
263
  const commentTestY = /<!--/y;
@@ -386,77 +391,84 @@ export class HTMLParser {
386
391
  cachedNextStartTag = null;
387
392
  cachedNextEndTag = null;
388
393
 
389
- // Comment
390
- commentTestY.lastIndex = pos;
391
- if (commentTestY.test(fullHtml)) {
392
- const commentEnd = commentEndAhead ? fullHtml.indexOf('-->', pos + 4) : -1;
393
- if (commentEnd === -1) commentEndAhead = false;
394
-
395
- if (commentEnd >= 0) {
396
- if (handler.comment) {
397
- const result = handler.comment(fullHtml.substring(pos + 4, commentEnd));
398
- if (isThenable(result)) await result;
394
+ // Dispatch on the character after `<`: Each branch below can only match
395
+ // with one leading character (`!` for markup declarations, `/` for end
396
+ // tags, a name character for start tags), so the matchers that require
397
+ // another one never run—a name lookup costs several regex executions
398
+ const nextCode = fullHtml.charCodeAt(pos + 1);
399
+ if (nextCode === 33) { // `!`
400
+ // Comment
401
+ commentTestY.lastIndex = pos;
402
+ if (commentTestY.test(fullHtml)) {
403
+ const commentEnd = commentEndAhead ? fullHtml.indexOf('-->', pos + 4) : -1;
404
+ if (commentEnd === -1) commentEndAhead = false;
405
+
406
+ if (commentEnd >= 0) {
407
+ if (handler.comment) {
408
+ const result = handler.comment(fullHtml.substring(pos + 4, commentEnd));
409
+ if (isThenable(result)) await result;
410
+ }
411
+ advance(commentEnd + 3 - pos);
412
+ prevTag = '';
413
+ prevAttrs = [];
414
+ continue;
399
415
  }
400
- advance(commentEnd + 3 - pos);
401
- prevTag = '';
402
- prevAttrs = [];
403
- continue;
404
416
  }
405
- }
406
417
 
407
- // https://web.archive.org/web/20241201212701/https://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
408
- conditionalTestY.lastIndex = pos;
409
- if (conditionalTestY.test(fullHtml)) {
410
- const conditionalEnd = conditionalEndAhead ? fullHtml.indexOf(']>', pos + 3) : -1;
411
- if (conditionalEnd === -1) conditionalEndAhead = false;
418
+ // https://web.archive.org/web/20241201212701/https://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
419
+ conditionalTestY.lastIndex = pos;
420
+ if (conditionalTestY.test(fullHtml)) {
421
+ const conditionalEnd = conditionalEndAhead ? fullHtml.indexOf(']>', pos + 3) : -1;
422
+ if (conditionalEnd === -1) conditionalEndAhead = false;
412
423
 
413
- if (conditionalEnd >= 0) {
414
- if (handler.comment) {
415
- const result = handler.comment(fullHtml.substring(pos + 2, conditionalEnd + 1), true /* Non-standard */);
416
- if (isThenable(result)) await result;
424
+ if (conditionalEnd >= 0) {
425
+ if (handler.comment) {
426
+ const result = handler.comment(fullHtml.substring(pos + 2, conditionalEnd + 1), true /* Non-standard */);
427
+ if (isThenable(result)) await result;
428
+ }
429
+ advance(conditionalEnd + 2 - pos);
430
+ prevTag = '';
431
+ prevAttrs = [];
432
+ continue;
417
433
  }
418
- advance(conditionalEnd + 2 - pos);
434
+ }
435
+
436
+ // Doctype
437
+ doctypeY.lastIndex = pos;
438
+ const doctypeMatch = doctypeY.exec(fullHtml);
439
+ if (doctypeMatch) {
440
+ if (handler.doctype) {
441
+ handler.doctype(doctypeMatch[0]);
442
+ }
443
+ advance(doctypeMatch[0].length);
419
444
  prevTag = '';
420
445
  prevAttrs = [];
421
446
  continue;
422
447
  }
423
- }
424
-
425
- // Doctype
426
- doctypeY.lastIndex = pos;
427
- const doctypeMatch = doctypeY.exec(fullHtml);
428
- if (doctypeMatch) {
429
- if (handler.doctype) {
430
- handler.doctype(doctypeMatch[0]);
448
+ } else if (nextCode === 47) { // `/`
449
+ // End tag—skip when no `>` lies ahead: The tag cannot be complete, and matching
450
+ // its name at every position would cost O(n²) on a long unclosed name
451
+ if (hasCloseAtOrAfter(pos)) {
452
+ const endTagMatch = matchEndTag(pos);
453
+ if (endTagMatch) {
454
+ advance(endTagMatch.text.length);
455
+ await parseEndTag(endTagMatch.text, endTagMatch.name);
456
+ prevTag = '/' + endTagMatch.name.toLowerCase();
457
+ prevAttrs = [];
458
+ continue;
459
+ }
431
460
  }
432
- advance(doctypeMatch[0].length);
433
- prevTag = '';
434
- prevAttrs = [];
435
- continue;
436
- }
437
-
438
- // End tag—skip when no `>` lies ahead: The tag cannot be complete, and matching
439
- // its name at every position would cost O(n²) on a long unclosed name
440
- if (hasCloseAtOrAfter(pos)) {
441
- const endTagMatch = matchEndTag(pos);
442
- if (endTagMatch) {
443
- advance(endTagMatch.text.length);
444
- await parseEndTag(endTagMatch.text, endTagMatch.name);
445
- prevTag = '/' + endTagMatch.name.toLowerCase();
446
- prevAttrs = [];
461
+ } else {
462
+ // Start tag
463
+ const startTagMatch = parseStartTag(pos);
464
+ if (startTagMatch) {
465
+ advance(startTagMatch.advance);
466
+ await handleStartTag(startTagMatch);
467
+ prevTag = startTagMatch.tagName.toLowerCase();
447
468
  continue;
448
469
  }
449
470
  }
450
471
 
451
- // Start tag
452
- const startTagMatch = parseStartTag(pos);
453
- if (startTagMatch) {
454
- advance(startTagMatch.advance);
455
- await handleStartTag(startTagMatch);
456
- prevTag = startTagMatch.tagName.toLowerCase();
457
- continue;
458
- }
459
-
460
472
  // Treat `<` as text
461
473
  if (handler.continueOnParseError) {
462
474
  // Continue looking for next tag
@@ -474,13 +486,21 @@ export class HTMLParser {
474
486
 
475
487
  // Next tag for whitespace processing context
476
488
  if (handler.wantsNextTag) {
477
- const nextStartTagMatch = parseStartTag(pos);
478
- if (nextStartTagMatch) {
479
- nextTag = nextStartTagMatch.tagName;
480
- // Extract minimal attribute info for whitespace logic (just name/value pairs)
481
- nextAttrs = extractAttrInfo(nextStartTagMatch.attrs);
482
- cachedNextStartTag = { match: nextStartTagMatch, pos };
483
- } else if (hasCloseAtOrAfter(pos)) {
489
+ // Same dispatch as above: A start tag cannot follow `<` + `/` or `!`,
490
+ // an end tag requires `/`—the other matcher never runs
491
+ const nextCode = fullHtml.charCodeAt(pos + 1);
492
+ if (nextCode !== 47 && nextCode !== 33) {
493
+ const nextStartTagMatch = parseStartTag(pos);
494
+ if (nextStartTagMatch) {
495
+ nextTag = nextStartTagMatch.tagName;
496
+ // Extract minimal attribute info for whitespace logic (just name/value pairs)
497
+ nextAttrs = extractAttrInfo(nextStartTagMatch.attrs);
498
+ cachedNextStartTag = { match: nextStartTagMatch, pos };
499
+ } else {
500
+ nextTag = '';
501
+ nextAttrs = [];
502
+ }
503
+ } else if (nextCode === 47 && hasCloseAtOrAfter(pos)) {
484
504
  const nextEndTagMatch = matchEndTag(pos);
485
505
  if (nextEndTagMatch) {
486
506
  nextTag = '/' + nextEndTagMatch.name;
@@ -507,7 +527,8 @@ export class HTMLParser {
507
527
  const isEscapableRawText = escapableRawTextElements.has(stackedTag);
508
528
 
509
529
  const remaining = sliceFromPos(pos);
510
- const rawText = findRawTextEnd(remaining, stackedTag);
530
+ // Where the raw text has no end, no end tag of its own ends the element either
531
+ const rawText = endlessRawTextElements.has(stackedTag) ? null : findRawTextEnd(remaining, stackedTag);
511
532
  if (rawText) {
512
533
  const text = rawText.content;
513
534
  if (handler.chars) {
@@ -534,6 +555,13 @@ export class HTMLParser {
534
555
  if (isThenable(result)) await result;
535
556
  }
536
557
  advance(remaining.length);
558
+ if (endlessRawTextElements.has(stackedTag)) {
559
+ // Nothing that stands behind the start tag is markup, so no end tag may be
560
+ // generated for the element, or for anything it stands in
561
+ stack.length = 0;
562
+ lastTag = '';
563
+ lastTagLower = '';
564
+ }
537
565
  }
538
566
  }
539
567
 
@@ -595,6 +623,23 @@ export class HTMLParser {
595
623
  return close === -1 ? null : { text: fullHtml.slice(startPos, close + 1), name: open[1] ?? '' };
596
624
  }
597
625
 
626
+ // Where a tag ends: After optional whitespace, at `>` or `/>`. Scanned by hand in a
627
+ // single pass, where a regex did the same with one execution per attribute.
628
+ function matchTagClose(/** @type {number} */ currentPos) {
629
+ let scan = currentPos;
630
+ let code = fullHtml.charCodeAt(scan);
631
+ while (isWhitespaceCode(code)) {
632
+ code = fullHtml.charCodeAt(++scan);
633
+ }
634
+ if (code === 62) { // `>`
635
+ return { len: scan - currentPos + 1, slash: '' };
636
+ }
637
+ if (code === 47 && fullHtml.charCodeAt(scan + 1) === 62) { // `/>`
638
+ return { len: scan - currentPos + 2, slash: '/' };
639
+ }
640
+ return null;
641
+ }
642
+
598
643
  function parseStartTag(/** @type {number} */ startPos) {
599
644
  startTagOpenY.lastIndex = startPos;
600
645
  const start = startTagOpenY.exec(fullHtml);
@@ -607,17 +652,16 @@ export class HTMLParser {
607
652
  };
608
653
  let consumed = start[0].length;
609
654
  let currentPos = startPos + consumed;
610
- let end, attr;
655
+ let close, attr;
611
656
 
612
657
  // Safety limit: Max length of input to check for attributes
613
658
  // Protects against catastrophic backtracking on massive attribute values
614
659
  const MAX_ATTR_PARSE_LENGTH = 20000; // 20 KB should be enough for any reasonable tag
615
660
 
616
661
  while (true) {
617
- // Check for closing tag first (sticky regex—no slicing)
618
- startTagCloseY.lastIndex = currentPos;
619
- end = startTagCloseY.exec(fullHtml);
620
- if (end) {
662
+ // Check for closing tag first (manual scan, regex only for the unusual)
663
+ close = matchTagClose(currentPos);
664
+ if (close) {
621
665
  break;
622
666
  }
623
667
 
@@ -740,12 +784,13 @@ export class HTMLParser {
740
784
  match.attrs.push(attr);
741
785
  }
742
786
 
743
- // Check for closing tag (sticky regex—no slicing)
744
- startTagCloseY.lastIndex = currentPos;
745
- end = startTagCloseY.exec(fullHtml);
746
- if (end) {
747
- match.unarySlash = end[1] ?? '';
748
- consumed += (end[0] ?? '').length;
787
+ // Check for closing tag (manual scan, regex only for the unusual)
788
+ if (!close) {
789
+ close = matchTagClose(currentPos);
790
+ }
791
+ if (close) {
792
+ match.unarySlash = close.slash;
793
+ consumed += close.len;
749
794
  match.advance = consumed;
750
795
  return match;
751
796
  }
@@ -190,7 +190,12 @@ const specialContentElements = new Set(['script', 'style']);
190
190
  // (`noscript`, `noframes`, and `noembed` are raw text, too, but stay markup here: What they
191
191
  // hold is markup to the UA that displays it—one without scripting or frames—so minifying it
192
192
  // as markup keeps what that UA sees)
193
- const genericRawTextElements = new Set(['iframe', 'xmp']);
193
+ const genericRawTextElements = new Set(['iframe', 'plaintext', 'xmp']);
194
+
195
+ // `plaintext` is the one element among these whose content has no end: The text runs to the
196
+ // end of the document, `</plaintext>` and all, as nothing switches the parser back to markup
197
+ // https://html.spec.whatwg.org/multipage/parsing.html#plaintext-state
198
+ const endlessRawTextElements = new Set(['plaintext']);
194
199
 
195
200
  // Escapable raw text elements, whose content is text rather than markup
196
201
  // https://html.spec.whatwg.org/multipage/syntax.html#elements-2
@@ -272,6 +277,7 @@ export {
272
277
  // Special content elements
273
278
  specialContentElements,
274
279
  genericRawTextElements,
280
+ endlessRawTextElements,
275
281
  escapableRawTextElements,
276
282
  rawTextElements
277
283
  };
@@ -183,7 +183,7 @@ const optionDefinitions = {
183
183
  type: 'boolean'
184
184
  },
185
185
  removeUnusedCSS: {
186
- description: 'Remove rules from `style` elements whose class or ID selectors the document never references (requires `--minify-css`); note that class names only applied by external scripts cannot be detected—use `{"safelist": […]}` for those',
186
+ description: 'Remove rules from `style` elements whose class or ID selectors the document never references—use with `--minify-css`; note that class names only applied by external scripts cannot be detected—use `{"safelist": […]}` for those',
187
187
  type: 'jsonObject'
188
188
  },
189
189
  sortAttributes: {
@@ -169,6 +169,14 @@ function isHiddenInput(tagName, attrs) {
169
169
  * @param {Set<string>} inlineTextSet
170
170
  */
171
171
  function collapseWhitespaceSmart(str, prevTag, nextTag, prevAttrs, nextAttrs, options, inlineElements, inlineTextSet) {
172
+ if (!str) return str;
173
+
174
+ // Fast path: No whitespace at all—every decision below would leave `str`
175
+ // untouched (`collapseWhitespace` returns such strings unchanged)
176
+ if (!/[ \n\r\t\f\xA0]/.test(str)) {
177
+ return str;
178
+ }
179
+
172
180
  const prevTagName = prevTag && (prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag);
173
181
  const nextTagName = nextTag && (nextTag.charAt(0) === '/' ? nextTag.slice(1) : nextTag);
174
182
 
@@ -178,14 +186,17 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, prevAttrs, nextAttrs, op
178
186
 
179
187
  let trimLeft = prevTag && !inlineElementsToKeepWhitespace.has(prevTag);
180
188
 
189
+ // Whether the text is whitespace only—one scan, shared by the checks below
190
+ // (`str` is known non-empty here)
191
+ const isPureWhitespace = !/\S/.test(str);
192
+
181
193
  // Smart default behavior: Collapse space after non-rendering elements (`type="hidden"`)
182
194
  // This happens even in basic `collapseWhitespace` mode (safe optimization)
183
- if (!trimLeft && prevIsHidden && str && !/\S/.test(str)) {
195
+ if (!trimLeft && prevIsHidden && isPureWhitespace) {
184
196
  trimLeft = true;
185
197
  }
186
198
 
187
199
  // Aggressive mode: Collapse between all form controls (pure whitespace only)
188
- const isPureWhitespace = str && !/\S/.test(str);
189
200
  if (!trimLeft && prevTagName && nextTagName &&
190
201
  options.collapseInlineTagWhitespace &&
191
202
  isPureWhitespace &&
@@ -208,7 +219,7 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, prevAttrs, nextAttrs, op
208
219
  let trimRight = nextTag && !inlineElementsToKeepWhitespace.has(nextTag);
209
220
 
210
221
  // Smart default behavior: Collapse space before non-rendering elements (`type="hidden"`)
211
- if (!trimRight && nextIsHidden && str && !/\S/.test(str)) {
222
+ if (!trimRight && nextIsHidden && isPureWhitespace) {
212
223
  trimRight = true;
213
224
  }
214
225