html-minifier-next 5.0.0 → 5.0.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 +23 -23
- package/cli.js +26 -67
- package/dist/htmlminifier.cjs +77 -31
- package/dist/htmlminifier.esm.bundle.js +77 -31
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/lib/option-definitions.d.ts +293 -0
- package/dist/types/lib/option-definitions.d.ts.map +1 -0
- package/dist/types/lib/options.d.ts.map +1 -1
- package/dist/types/lib/utils.d.ts +2 -0
- package/dist/types/lib/utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/htmlminifier.js +41 -10
- package/src/lib/option-definitions.js +207 -0
- package/src/lib/options.js +4 -22
- package/src/lib/utils.js +22 -2
package/README.md
CHANGED
|
@@ -402,39 +402,39 @@ How does HTML Minifier Next compare to other minifiers? (All minification with t
|
|
|
402
402
|
<!-- Auto-generated benchmarks, don’t edit -->
|
|
403
403
|
| Site | Original Size (KB) | [HTML Minifier Next](https://github.com/j9t/html-minifier-next) ([config](https://github.com/j9t/html-minifier-next/blob/main/benchmarks/html-minifier.json))<br>[](https://socket.dev/npm/package/html-minifier-next) | [htmlnano](https://github.com/posthtml/htmlnano)<br>[](https://socket.dev/npm/package/htmlnano) | [@swc/html](https://github.com/swc-project/swc)<br>[](https://socket.dev/npm/package/@swc/html) | [minify-html](https://github.com/wilsonzlin/minify-html)<br>[](https://socket.dev/npm/package/@minify-html/node) | [minimize](https://github.com/Swaagie/minimize)<br>[](https://socket.dev/npm/package/minimize) | [htmlcompressor.com](https://htmlcompressor.com/) |
|
|
404
404
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
405
|
-
| [A List Apart](https://alistapart.com/) |
|
|
406
|
-
| [Apple](https://www.apple.com/) |
|
|
407
|
-
| [BBC](https://www.bbc.co.uk/) |
|
|
408
|
-
| [CERN](https://home.cern/) |
|
|
409
|
-
| [CSS-Tricks](https://css-tricks.com/) |
|
|
405
|
+
| [A List Apart](https://alistapart.com/) | 63 | **53** | 55 | 56 | 55 | 58 | 56 |
|
|
406
|
+
| [Apple](https://www.apple.com/) | 236 | **197** | 209 | 212 | 213 | 215 | 215 |
|
|
407
|
+
| [BBC](https://www.bbc.co.uk/) | 647 | **601** | 607 | 608 | 609 | 642 | n/a |
|
|
408
|
+
| [CERN](https://home.cern/) | 150 | **82** | 90 | 90 | 90 | 92 | 95 |
|
|
409
|
+
| [CSS-Tricks](https://css-tricks.com/) | 155 | 127 | **121** | 136 | 137 | 141 | 138 |
|
|
410
410
|
| [ECMAScript](https://tc39.es/ecma262/) | 7261 | **6411** | 6583 | 6465 | 6589 | 6637 | n/a |
|
|
411
|
-
| [EDRi](https://edri.org/) | 80 | **
|
|
412
|
-
| [EFF](https://www.eff.org/) | 54 | **
|
|
411
|
+
| [EDRi](https://edri.org/) | 80 | **68** | 69 | 69 | 71 | 74 | 72 |
|
|
412
|
+
| [EFF](https://www.eff.org/) | 54 | **45** | 48 | 47 | 48 | 49 | 49 |
|
|
413
413
|
| [European Alternatives](https://european-alternatives.eu/) | 48 | **30** | 32 | 32 | 32 | 32 | 32 |
|
|
414
|
-
| [FAZ](https://www.faz.net/aktuell/) |
|
|
414
|
+
| [FAZ](https://www.faz.net/aktuell/) | 1545 | 1412 | **1385** | 1471 | 1482 | 1492 | n/a |
|
|
415
415
|
| [French Tech](https://lafrenchtech.gouv.fr/) | 153 | **122** | 126 | 126 | 126 | 132 | 127 |
|
|
416
|
-
| [Frontend Dogma](https://frontenddogma.com/) | 227 | **219** |
|
|
417
|
-
| [Google](https://www.google.com/) | 18 | **16** |
|
|
418
|
-
| [Ground News](https://ground.news/) |
|
|
416
|
+
| [Frontend Dogma](https://frontenddogma.com/) | 227 | **219** | 240 | 225 | 227 | 246 | 227 |
|
|
417
|
+
| [Google](https://www.google.com/) | 18 | **16** | **16** | **16** | 17 | 18 | 18 |
|
|
418
|
+
| [Ground News](https://ground.news/) | 1513 | **1371** | 1394 | 1418 | 1424 | 1499 | n/a |
|
|
419
419
|
| [HTML Living Standard](https://html.spec.whatwg.org/multipage/) | 149 | 148 | 153 | **147** | 149 | 155 | 149 |
|
|
420
|
-
| [Igalia](https://www.igalia.com/) | 49 | **
|
|
421
|
-
| [Leanpub](https://leanpub.com/) |
|
|
422
|
-
| [Mastodon](https://mastodon.social/explore) | 38 |
|
|
420
|
+
| [Igalia](https://www.igalia.com/) | 49 | **34** | 36 | 36 | 36 | 37 | 37 |
|
|
421
|
+
| [Leanpub](https://leanpub.com/) | 243 | **226** | 229 | 228 | 229 | 239 | 240 |
|
|
422
|
+
| [Mastodon](https://mastodon.social/explore) | 38 | 35 | **32** | 35 | 36 | 37 | 37 |
|
|
423
423
|
| [MDN](https://developer.mozilla.org/en-US/) | 109 | **62** | 64 | 65 | 65 | 68 | 68 |
|
|
424
|
-
| [Middle East Eye](https://www.middleeasteye.net/) |
|
|
425
|
-
| [Mistral AI](https://mistral.ai/) | 343 | **
|
|
424
|
+
| [Middle East Eye](https://www.middleeasteye.net/) | 220 | **194** | 200 | 198 | 198 | 199 | 200 |
|
|
425
|
+
| [Mistral AI](https://mistral.ai/) | 343 | **307** | **307** | 310 | 311 | 340 | n/a |
|
|
426
426
|
| [Mozilla](https://www.mozilla.org/) | 47 | **32** | 35 | 35 | 35 | 36 | 36 |
|
|
427
|
-
| [Nielsen Norman Group](https://www.nngroup.com/) | 97 |
|
|
428
|
-
| [SitePoint](https://www.sitepoint.com/) |
|
|
427
|
+
| [Nielsen Norman Group](https://www.nngroup.com/) | 97 | 73 | **59** | 78 | 80 | 81 | 81 |
|
|
428
|
+
| [SitePoint](https://www.sitepoint.com/) | 495 | 456 | **431** | 468 | 473 | 491 | n/a |
|
|
429
429
|
| [Startup-Verband](https://startupverband.de/) | 43 | **30** | 31 | **30** | 31 | 31 | 31 |
|
|
430
|
-
| [TetraLogical](https://tetralogical.com/) | 59 |
|
|
430
|
+
| [TetraLogical](https://tetralogical.com/) | 59 | 52 | **49** | 51 | 53 | 53 | 53 |
|
|
431
431
|
| [TPGi](https://www.tpgi.com/) | 173 | **157** | 159 | 163 | 164 | 170 | 170 |
|
|
432
|
-
| [United Nations](https://www.un.org/en/) | 151 | **113** | 121 | 125 |
|
|
433
|
-
| [Vivaldi](https://vivaldi.com/) | 93 | **
|
|
432
|
+
| [United Nations](https://www.un.org/en/) | 151 | **113** | 121 | 125 | 124 | 130 | 123 |
|
|
433
|
+
| [Vivaldi](https://vivaldi.com/) | 93 | **75** | n/a | 79 | 81 | 84 | 82 |
|
|
434
434
|
| [W3C](https://www.w3.org/) | 50 | **36** | 39 | 38 | 38 | 41 | 39 |
|
|
435
|
-
| **Average processing time** | |
|
|
435
|
+
| **Average processing time** | | 77 ms (30/30) | 149 ms (29/30) | 49 ms (30/30) | **17 ms (30/30)** | 330 ms (30/30) | 1513 ms (24/30) |
|
|
436
436
|
|
|
437
|
-
(Last updated: Feb
|
|
437
|
+
(Last updated: Feb 3, 2026)
|
|
438
438
|
<!-- End auto-generated -->
|
|
439
439
|
|
|
440
440
|
Notes: Minimize does not minify CSS and JS. [HTML Minifier Terser](https://github.com/terser/html-minifier-terser) is currently not included due to issues around whitespace collapsing and removal of code using modern CSS features, issues which appeared to distort the data.
|
package/cli.js
CHANGED
|
@@ -41,6 +41,8 @@ const camelCase = (str) => paramCase(str).replace(/-([a-z])/g, (_, c) => c.toUpp
|
|
|
41
41
|
|
|
42
42
|
// Lazy-load HMN to reduce CLI cold-start overhead
|
|
43
43
|
import { getPreset, getPresetNames } from './src/presets.js';
|
|
44
|
+
import { parseRegExp } from './src/lib/utils.js';
|
|
45
|
+
import { optionDefinitions } from './src/lib/option-definitions.js';
|
|
44
46
|
|
|
45
47
|
const require = createRequire(import.meta.url);
|
|
46
48
|
const pkg = require('./package.json');
|
|
@@ -49,7 +51,6 @@ const DEFAULT_FILE_EXTENSIONS = ['html', 'htm', 'xhtml', 'shtml'];
|
|
|
49
51
|
|
|
50
52
|
const program = new Command();
|
|
51
53
|
program.name(pkg.name);
|
|
52
|
-
program.version(pkg.version);
|
|
53
54
|
|
|
54
55
|
function fatal(message) {
|
|
55
56
|
console.error(message);
|
|
@@ -82,11 +83,6 @@ process.stdout.on('error', (err) => {
|
|
|
82
83
|
*
|
|
83
84
|
* --customAttrSurround "[\"//matchString//\"]"
|
|
84
85
|
*/
|
|
85
|
-
function parseRegExp(value) {
|
|
86
|
-
if (value) {
|
|
87
|
-
return new RegExp(value.replace(/^\/(.*)\/$/, '$1'));
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
86
|
|
|
91
87
|
function parseJSON(value) {
|
|
92
88
|
if (value) {
|
|
@@ -125,68 +121,28 @@ const parseValidInt = (optionName) => (value) => {
|
|
|
125
121
|
return num;
|
|
126
122
|
};
|
|
127
123
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
continueOnParseError: 'Handle parse errors instead of aborting',
|
|
137
|
-
customAttrAssign: ['Array of regexes that allow to support custom attribute assign expressions (e.g., `<div flex?="{{mode != cover}}"></div>`)', parseJSONRegExpArray],
|
|
138
|
-
customAttrCollapse: ['Regex that specifies custom attribute to strip newlines from (e.g., /ng-class/)', parseRegExp],
|
|
139
|
-
customAttrSurround: ['Array of regexes that allow to support custom attribute surround expressions (e.g., `<input {{#if value}}checked="checked"{{/if}}>`)', parseJSONRegExpArray],
|
|
140
|
-
customEventAttributes: ['Array of regexes that allow to support custom event attributes for minifyJS (e.g., `ng-click`)', parseJSONRegExpArray],
|
|
141
|
-
customFragmentQuantifierLimit: ['Set maximum quantifier limit for custom fragments to prevent ReDoS attacks (default: 200)', parseValidInt('customFragmentQuantifierLimit')],
|
|
142
|
-
decodeEntities: 'Use direct Unicode characters whenever possible',
|
|
143
|
-
ignoreCustomComments: ['Array of regexes that allow to ignore matching comments', parseJSONRegExpArray],
|
|
144
|
-
ignoreCustomFragments: ['Array of regexes that allow to ignore certain fragments, when matched (e.g., `<?php … ?>`, `{{ … }}`)', parseJSONRegExpArray],
|
|
145
|
-
includeAutoGeneratedTags: 'Insert elements generated by HTML parser',
|
|
146
|
-
inlineCustomElements: ['Array of names of custom elements which are inline, for whitespace handling', parseJSONArray],
|
|
147
|
-
keepClosingSlash: 'Keep the trailing slash on void elements',
|
|
148
|
-
maxInputLength: ['Maximum input length to prevent ReDoS attacks', parseValidInt('maxInputLength')],
|
|
149
|
-
maxLineLength: ['Specify a maximum line length; compressed output will be split by newlines at valid HTML split-points', parseValidInt('maxLineLength')],
|
|
150
|
-
mergeScripts: 'Merge consecutive inline `script` elements into one',
|
|
151
|
-
minifyCSS: ['Minify CSS in `style` elements and attributes (uses Lightning CSS)', parseJSON],
|
|
152
|
-
minifyJS: ['Minify JavaScript in `script` elements and event attributes (uses Terser or SWC; pass `{"engine": "swc"}` for SWC)', parseJSON],
|
|
153
|
-
minifySVG: ['Minify SVG elements and attributes (numeric precision, default attributes, colors)', parseJSON],
|
|
154
|
-
minifyURLs: ['Minify URLs in various attributes', parseJSON],
|
|
155
|
-
noNewlinesBeforeTagClose: 'Never add a newline before a tag that closes an element',
|
|
156
|
-
partialMarkup: 'Treat input as a partial HTML fragment, preserving stray end tags and unclosed tags',
|
|
157
|
-
preserveLineBreaks: 'Always collapse to one line break (never remove it entirely) when whitespace between tags includes a line break—use with `--collapse-whitespace`',
|
|
158
|
-
preventAttributesEscaping: 'Prevents the escaping of the values of attributes',
|
|
159
|
-
processConditionalComments: 'Process contents of conditional comments through minifier',
|
|
160
|
-
processScripts: ['Array of strings corresponding to types of `script` elements to process through minifier (e.g., `text/ng-template`, `text/x-handlebars-template`, etc.)', parseJSONArray],
|
|
161
|
-
quoteCharacter: ['Type of quote to use for attribute values (“\'” or “"”)', parseString],
|
|
162
|
-
removeAttributeQuotes: 'Remove quotes around attributes when possible',
|
|
163
|
-
removeComments: 'Strip HTML comments',
|
|
164
|
-
removeEmptyAttributes: 'Remove all attributes with whitespace-only values',
|
|
165
|
-
removeEmptyElements: 'Remove all elements with empty contents',
|
|
166
|
-
removeEmptyElementsExcept: ['Array of elements to preserve when `--remove-empty-elements` is enabled (e.g., `td`, `["td", "<span aria-hidden=\'true\'>"]`)', parseJSONArray],
|
|
167
|
-
removeOptionalTags: 'Remove unrequired tags',
|
|
168
|
-
removeRedundantAttributes: 'Remove attributes when value matches default',
|
|
169
|
-
removeScriptTypeAttributes: 'Remove `type="text/javascript"` from `script` elements; other `type` attribute values are left intact',
|
|
170
|
-
removeStyleLinkTypeAttributes: 'Remove `type="text/css"` from `style` and `link` elements; other `type` attribute values are left intact',
|
|
171
|
-
removeTagWhitespace: 'Remove space between attributes whenever possible; note that this will result in invalid HTML',
|
|
172
|
-
sortAttributes: 'Sort attributes by frequency',
|
|
173
|
-
sortClassNames: 'Sort style classes by frequency',
|
|
174
|
-
trimCustomFragments: 'Trim whitespace around custom fragments (`--ignore-custom-fragments`)',
|
|
175
|
-
useShortDoctype: 'Replaces the doctype with the short HTML doctype'
|
|
124
|
+
// Map option types to CLI parsers
|
|
125
|
+
const typeParsers = {
|
|
126
|
+
regexp: parseRegExp,
|
|
127
|
+
regexpArray: parseJSONRegExpArray,
|
|
128
|
+
json: parseJSON,
|
|
129
|
+
jsonArray: parseJSONArray,
|
|
130
|
+
string: parseString,
|
|
131
|
+
int: (key) => parseValidInt(key)
|
|
176
132
|
};
|
|
177
133
|
|
|
178
|
-
// Configure command-line flags
|
|
179
|
-
const mainOptionKeys = Object.keys(
|
|
134
|
+
// Configure command-line flags from shared option definitions
|
|
135
|
+
const mainOptionKeys = Object.keys(optionDefinitions);
|
|
180
136
|
mainOptionKeys.forEach(function (key) {
|
|
181
|
-
const
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
program.option(key,
|
|
186
|
-
} else if (key === 'continueOnMinifyError') {
|
|
187
|
-
program.option('--no-' + paramCase(key), option);
|
|
137
|
+
const { description, type } = optionDefinitions[key];
|
|
138
|
+
if (type === 'invertedBoolean') {
|
|
139
|
+
program.option('--no-' + paramCase(key), description);
|
|
140
|
+
} else if (type === 'boolean') {
|
|
141
|
+
program.option('--' + paramCase(key), description);
|
|
188
142
|
} else {
|
|
189
|
-
|
|
143
|
+
const flag = '--' + paramCase(key) + (type === 'json' ? ' [value]' : ' <value>');
|
|
144
|
+
const parser = type === 'int' ? typeParsers.int(key) : typeParsers[type];
|
|
145
|
+
program.option(flag, description, parser);
|
|
190
146
|
}
|
|
191
147
|
});
|
|
192
148
|
program.option('-o --output <file>', 'Specify output file (reads from file arguments or STDIN; outputs to STDOUT if not specified)');
|
|
@@ -252,10 +208,11 @@ function normalizeConfig(config) {
|
|
|
252
208
|
// Apply parsers to main options
|
|
253
209
|
mainOptionKeys.forEach(function (key) {
|
|
254
210
|
if (key in normalized) {
|
|
255
|
-
const
|
|
256
|
-
if (
|
|
211
|
+
const { type } = optionDefinitions[key];
|
|
212
|
+
if (type !== 'boolean' && type !== 'invertedBoolean') {
|
|
213
|
+
const parser = type === 'int' ? typeParsers.int(key) : typeParsers[type];
|
|
257
214
|
const value = normalized[key];
|
|
258
|
-
normalized[key] =
|
|
215
|
+
normalized[key] = parser(typeof value === 'string' ? value : JSON.stringify(value));
|
|
259
216
|
}
|
|
260
217
|
}
|
|
261
218
|
});
|
|
@@ -288,6 +245,8 @@ program.option('-p --preset <name>', `Use a preset configuration (${getPresetNam
|
|
|
288
245
|
program.option('-c --config-file <file>', 'Use config file');
|
|
289
246
|
program.option('--cache-css <size>', 'Set CSS minification cache size (number of entries, default: 500)', parseValidInt('cacheCSS'));
|
|
290
247
|
program.option('--cache-js <size>', 'Set JavaScript minification cache size (number of entries, default: 500)', parseValidInt('cacheJS'));
|
|
248
|
+
program.version(pkg.version, '-V, --version', 'Output the version number');
|
|
249
|
+
program.helpOption('-h, --help', 'Display help for command');
|
|
291
250
|
|
|
292
251
|
(async () => {
|
|
293
252
|
let content;
|
package/dist/htmlminifier.cjs
CHANGED
|
@@ -966,7 +966,7 @@ function uniqueId(value) {
|
|
|
966
966
|
return id;
|
|
967
967
|
}
|
|
968
968
|
|
|
969
|
-
// Identity functions
|
|
969
|
+
// Identity and transform functions
|
|
970
970
|
|
|
971
971
|
function identity(value) {
|
|
972
972
|
return value;
|
|
@@ -976,6 +976,10 @@ function identityAsync(value) {
|
|
|
976
976
|
return Promise.resolve(value);
|
|
977
977
|
}
|
|
978
978
|
|
|
979
|
+
function lowercase(value) {
|
|
980
|
+
return value.toLowerCase();
|
|
981
|
+
}
|
|
982
|
+
|
|
979
983
|
// Replace async helper
|
|
980
984
|
|
|
981
985
|
/**
|
|
@@ -997,6 +1001,20 @@ async function replaceAsync(str, regex, asyncFn) {
|
|
|
997
1001
|
return str.replace(regex, () => data.shift());
|
|
998
1002
|
}
|
|
999
1003
|
|
|
1004
|
+
// String patterns to RegExp conversion (for JSON config support)
|
|
1005
|
+
|
|
1006
|
+
function parseRegExp(value) {
|
|
1007
|
+
if (typeof value === 'string') {
|
|
1008
|
+
if (!value) return undefined; // Empty string = not configured
|
|
1009
|
+
const match = value.match(/^\/(.+)\/([dgimsuvy]*)$/);
|
|
1010
|
+
if (match) {
|
|
1011
|
+
return new RegExp(match[1], match[2]);
|
|
1012
|
+
}
|
|
1013
|
+
return new RegExp(value);
|
|
1014
|
+
}
|
|
1015
|
+
return value;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1000
1018
|
// Regex patterns (to avoid repeated allocations in hot paths)
|
|
1001
1019
|
|
|
1002
1020
|
const RE_WS_START = /^[ \n\r\t\f]+/;
|
|
@@ -2041,6 +2059,22 @@ function getSVGMinifierOptions(userOptions) {
|
|
|
2041
2059
|
return null;
|
|
2042
2060
|
}
|
|
2043
2061
|
|
|
2062
|
+
// Single source of truth for minifier option names, descriptions, types, and shared defaults
|
|
2063
|
+
|
|
2064
|
+
|
|
2065
|
+
const optionDefaults = {
|
|
2066
|
+
continueOnMinifyError: true,
|
|
2067
|
+
ignoreCustomComments: [
|
|
2068
|
+
/^!/,
|
|
2069
|
+
/^\s*#/
|
|
2070
|
+
],
|
|
2071
|
+
ignoreCustomFragments: [
|
|
2072
|
+
/<%[\s\S]*?%>/,
|
|
2073
|
+
/<\?[\s\S]*?\?>/
|
|
2074
|
+
],
|
|
2075
|
+
includeAutoGeneratedTags: false
|
|
2076
|
+
};
|
|
2077
|
+
|
|
2044
2078
|
// Imports
|
|
2045
2079
|
|
|
2046
2080
|
|
|
@@ -2072,21 +2106,10 @@ function shouldMinifyInnerHTML(options) {
|
|
|
2072
2106
|
*/
|
|
2073
2107
|
const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssMinifyCache, jsMinifyCache } = {}) => {
|
|
2074
2108
|
const options = {
|
|
2075
|
-
name:
|
|
2076
|
-
return name.toLowerCase();
|
|
2077
|
-
},
|
|
2109
|
+
name: lowercase,
|
|
2078
2110
|
canCollapseWhitespace,
|
|
2079
2111
|
canTrimWhitespace,
|
|
2080
|
-
|
|
2081
|
-
ignoreCustomComments: [
|
|
2082
|
-
/^!/,
|
|
2083
|
-
/^\s*#/
|
|
2084
|
-
],
|
|
2085
|
-
ignoreCustomFragments: [
|
|
2086
|
-
/<%[\s\S]*?%>/,
|
|
2087
|
-
/<\?[\s\S]*?\?>/
|
|
2088
|
-
],
|
|
2089
|
-
includeAutoGeneratedTags: false,
|
|
2112
|
+
...optionDefaults,
|
|
2090
2113
|
log: identity,
|
|
2091
2114
|
minifyCSS: identityAsync,
|
|
2092
2115
|
minifyJS: identity,
|
|
@@ -2094,14 +2117,6 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
2094
2117
|
minifySVG: null
|
|
2095
2118
|
};
|
|
2096
2119
|
|
|
2097
|
-
// Helper to convert string patterns to RegExp (for JSON config support)
|
|
2098
|
-
const parseRegExp = (value) => {
|
|
2099
|
-
if (typeof value === 'string') {
|
|
2100
|
-
return new RegExp(value.replace(/^\/(.*)\/$/, '$1'));
|
|
2101
|
-
}
|
|
2102
|
-
return value; // Already a RegExp or another type
|
|
2103
|
-
};
|
|
2104
|
-
|
|
2105
2120
|
const parseRegExpArray = (arr) => {
|
|
2106
2121
|
return Array.isArray(arr) ? arr.map(parseRegExp) : arr;
|
|
2107
2122
|
};
|
|
@@ -3231,6 +3246,9 @@ const DEFAULT_JS_TYPES = new Set(['', 'text/javascript', 'application/javascript
|
|
|
3231
3246
|
const RE_START_TAG = /^<[^/!]/;
|
|
3232
3247
|
const RE_END_TAG = /^<\//;
|
|
3233
3248
|
|
|
3249
|
+
// HTML encoding types for annotation-xml (MathML)
|
|
3250
|
+
const RE_HTML_ENCODING = /^(text\/html|application\/xhtml\+xml)$/i;
|
|
3251
|
+
|
|
3234
3252
|
// Script merging
|
|
3235
3253
|
|
|
3236
3254
|
/**
|
|
@@ -3993,6 +4011,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3993
4011
|
const stackNoCollapseWhitespace = [];
|
|
3994
4012
|
let optionalStartTag = '';
|
|
3995
4013
|
let optionalEndTag = '';
|
|
4014
|
+
let optionalEndTagEmitted = false;
|
|
3996
4015
|
const ignoredMarkupChunks = [];
|
|
3997
4016
|
const ignoredCustomMarkupChunks = [];
|
|
3998
4017
|
let uidIgnore;
|
|
@@ -4173,8 +4192,24 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4173
4192
|
options.keepClosingSlash = true;
|
|
4174
4193
|
options.name = identity;
|
|
4175
4194
|
options.insideSVG = lowerTag === 'svg';
|
|
4195
|
+
options.insideForeignContent = true;
|
|
4176
4196
|
}
|
|
4177
|
-
|
|
4197
|
+
// `foreignObject` in SVG and `annotation-xml` in MathML contain HTML content
|
|
4198
|
+
// Note: The element itself is in SVG/MathML namespace, only its children are HTML
|
|
4199
|
+
let useParentNameForTag = false;
|
|
4200
|
+
if (options.insideForeignContent && (lowerTag === 'foreignobject' ||
|
|
4201
|
+
(lowerTag === 'annotation-xml' && attrs.some(a => a.name.toLowerCase() === 'encoding' &&
|
|
4202
|
+
RE_HTML_ENCODING.test(a.value))))) {
|
|
4203
|
+
const parentName = options.name;
|
|
4204
|
+
options = Object.create(options);
|
|
4205
|
+
options.caseSensitive = false;
|
|
4206
|
+
options.keepClosingSlash = false;
|
|
4207
|
+
options.parentName = parentName; // Preserve for the element tag itself
|
|
4208
|
+
options.name = options.htmlName || lowercase;
|
|
4209
|
+
options.insideForeignContent = false;
|
|
4210
|
+
useParentNameForTag = true;
|
|
4211
|
+
}
|
|
4212
|
+
tag = (useParentNameForTag ? options.parentName : options.name)(tag);
|
|
4178
4213
|
currentTag = tag;
|
|
4179
4214
|
charsPrevTag = tag;
|
|
4180
4215
|
if (!inlineTextSet.has(tag)) {
|
|
@@ -4197,12 +4232,15 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4197
4232
|
optionalStartTag = '';
|
|
4198
4233
|
// End-tag-followed-by-start-tag omission rules
|
|
4199
4234
|
if (htmlTag && canRemovePrecedingTag(optionalEndTag, tag)) {
|
|
4200
|
-
|
|
4235
|
+
if (optionalEndTagEmitted) {
|
|
4236
|
+
removeEndTag();
|
|
4237
|
+
}
|
|
4201
4238
|
// `<colgroup>` cannot be omitted if preceding `</colgroup>` is omitted
|
|
4202
4239
|
// `<tbody>` cannot be omitted if preceding `</tbody>`, `</thead>`, or `</tfoot>` is omitted
|
|
4203
4240
|
optional = !isStartTagMandatory(optionalEndTag, tag);
|
|
4204
4241
|
}
|
|
4205
4242
|
optionalEndTag = '';
|
|
4243
|
+
optionalEndTagEmitted = false;
|
|
4206
4244
|
}
|
|
4207
4245
|
|
|
4208
4246
|
// Set whitespace flags for nested tags (e.g., `<code>` within a `<pre>`)
|
|
@@ -4256,8 +4294,12 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4256
4294
|
},
|
|
4257
4295
|
end: function (tag, attrs, autoGenerated) {
|
|
4258
4296
|
const lowerTag = tag.toLowerCase();
|
|
4297
|
+
// Restore parent context when exiting SVG/MathML or HTML-in-foreign-content elements
|
|
4259
4298
|
if (lowerTag === 'svg' || lowerTag === 'math') {
|
|
4260
4299
|
options = Object.getPrototypeOf(options);
|
|
4300
|
+
} else if ((lowerTag === 'foreignobject' || lowerTag === 'annotation-xml') &&
|
|
4301
|
+
!options.insideForeignContent && Object.getPrototypeOf(options).insideForeignContent) {
|
|
4302
|
+
options = Object.getPrototypeOf(options);
|
|
4261
4303
|
}
|
|
4262
4304
|
tag = options.name(tag);
|
|
4263
4305
|
|
|
@@ -4292,13 +4334,14 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4292
4334
|
// `</head>` may be omitted if not followed by space or comment
|
|
4293
4335
|
// `</p>` may be omitted if no more content in non-`</a>` parent
|
|
4294
4336
|
// except for `</dt>` or `</thead>`, end tags may be omitted if no more content in parent element
|
|
4295
|
-
if (tag && optionalEndTag && !trailingElements.has(optionalEndTag) && (optionalEndTag !== 'p' || !pInlineElements.has(tag))) {
|
|
4337
|
+
if (tag && optionalEndTag && optionalEndTagEmitted && !trailingElements.has(optionalEndTag) && (optionalEndTag !== 'p' || !pInlineElements.has(tag))) {
|
|
4296
4338
|
removeEndTag();
|
|
4297
4339
|
}
|
|
4298
4340
|
optionalEndTag = optionalEndTags.has(tag) ? tag : '';
|
|
4341
|
+
optionalEndTagEmitted = true;
|
|
4299
4342
|
}
|
|
4300
4343
|
|
|
4301
|
-
if (options.removeEmptyElements && isElementEmpty && canRemoveElement(tag, attrs)) {
|
|
4344
|
+
if (options.removeEmptyElements && isElementEmpty && !options.insideForeignContent && canRemoveElement(tag, attrs)) {
|
|
4302
4345
|
let preserve = false;
|
|
4303
4346
|
if (removeEmptyElementsExcept.length) {
|
|
4304
4347
|
// Normalize attribute names for comparison with specs
|
|
@@ -4311,10 +4354,11 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4311
4354
|
removeStartTag();
|
|
4312
4355
|
optionalStartTag = '';
|
|
4313
4356
|
optionalEndTag = '';
|
|
4357
|
+
optionalEndTagEmitted = false;
|
|
4314
4358
|
} else {
|
|
4315
4359
|
// Preserve the element—add closing tag
|
|
4316
4360
|
if (autoGenerated && !options.includeAutoGeneratedTags) {
|
|
4317
|
-
|
|
4361
|
+
optionalEndTagEmitted = false;
|
|
4318
4362
|
} else {
|
|
4319
4363
|
buffer.push('</' + tag + '>');
|
|
4320
4364
|
}
|
|
@@ -4327,7 +4371,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4327
4371
|
}
|
|
4328
4372
|
} else {
|
|
4329
4373
|
if (autoGenerated && !options.includeAutoGeneratedTags) {
|
|
4330
|
-
|
|
4374
|
+
optionalEndTagEmitted = false;
|
|
4331
4375
|
} else {
|
|
4332
4376
|
buffer.push('</' + tag + '>');
|
|
4333
4377
|
}
|
|
@@ -4424,12 +4468,13 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4424
4468
|
optionalStartTag = '';
|
|
4425
4469
|
// `</html>` or `</body>` may be omitted if not followed by comment
|
|
4426
4470
|
// `</head>`, `</colgroup>`, or `</caption>` may be omitted if not followed by space or comment
|
|
4427
|
-
if (compactElements.has(optionalEndTag) || (looseElements.has(optionalEndTag) && !/^\s/.test(text))) {
|
|
4471
|
+
if (optionalEndTagEmitted && (compactElements.has(optionalEndTag) || (looseElements.has(optionalEndTag) && !/^\s/.test(text)))) {
|
|
4428
4472
|
removeEndTag();
|
|
4429
4473
|
}
|
|
4430
|
-
// Don
|
|
4474
|
+
// Don't reset optionalEndTag if text is only whitespace and will be collapsed (not conservatively)
|
|
4431
4475
|
if (!/^\s+$/.test(text) || !options.collapseWhitespace || options.conservativeCollapse) {
|
|
4432
4476
|
optionalEndTag = '';
|
|
4477
|
+
optionalEndTagEmitted = false;
|
|
4433
4478
|
}
|
|
4434
4479
|
}
|
|
4435
4480
|
charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
|
|
@@ -4475,6 +4520,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4475
4520
|
// Preceding comments suppress tag omissions
|
|
4476
4521
|
optionalStartTag = '';
|
|
4477
4522
|
optionalEndTag = '';
|
|
4523
|
+
optionalEndTagEmitted = false;
|
|
4478
4524
|
}
|
|
4479
4525
|
|
|
4480
4526
|
// Optimize whitespace collapsing between consecutive `htmlmin:ignore` placeholder comments
|
|
@@ -4567,7 +4613,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4567
4613
|
removeStartTag();
|
|
4568
4614
|
}
|
|
4569
4615
|
// except for `</dt>` or `</thead>`, end tags may be omitted if no more content in parent element
|
|
4570
|
-
if (optionalEndTag && !trailingElements.has(optionalEndTag)) {
|
|
4616
|
+
if (optionalEndTag && optionalEndTagEmitted && !trailingElements.has(optionalEndTag)) {
|
|
4571
4617
|
removeEndTag();
|
|
4572
4618
|
}
|
|
4573
4619
|
}
|