html-minifier-next 4.6.1 → 4.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -33
- package/cli.js +4 -2
- package/dist/htmlminifier.cjs +146 -23
- package/dist/htmlminifier.esm.bundle.js +146 -23
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/htmlminifier.js +146 -23
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# HTML Minifier Next
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/html-minifier-next) [](https://github.com/j9t/html-minifier-next/actions)
|
|
3
|
+
[](https://www.npmjs.com/package/html-minifier-next) [](https://github.com/j9t/html-minifier-next/actions) [](https://socket.dev/npm/package/html-minifier-next)
|
|
4
4
|
|
|
5
5
|
HTML Minifier Next (HMN) is a highly **configurable, well-tested, JavaScript-based HTML minifier**.
|
|
6
6
|
|
|
@@ -146,7 +146,7 @@ Options can be used in config files (camelCase) or via CLI flags (kebab-case wit
|
|
|
146
146
|
| `customEventAttributes`<br>`--custom-event-attributes` | Arrays of regexes that allow to support custom event attributes for `minifyJS` (e.g., `ng-click`) | `[ /^on[a-z]{3,}$/ ]` |
|
|
147
147
|
| `customFragmentQuantifierLimit`<br>`--custom-fragment-quantifier-limit` | Set maximum quantifier limit for custom fragments to prevent ReDoS attacks | `200` |
|
|
148
148
|
| `decodeEntities`<br>`--decode-entities` | Use direct Unicode characters whenever possible | `false` |
|
|
149
|
-
| `html5`<br>`--no-html5` | Parse input according to the HTML specification; when `false`,
|
|
149
|
+
| `html5`<br>`--no-html5` | Parse input according to the HTML specification; when `false`, enforces legacy inline/block nesting rules that may restructure modern HTML | `true` |
|
|
150
150
|
| `ignoreCustomComments`<br>`--ignore-custom-comments` | Array of regexes that allow to ignore certain comments, when matched | `[ /^!/, /^\s*#/ ]` |
|
|
151
151
|
| `ignoreCustomFragments`<br>`--ignore-custom-fragments` | Array of regexes that allow to ignore certain fragments, when matched (e.g., `<?php … ?>`, `{{ … }}`, etc.) | `[ /<%[\s\S]*?%>/, /<\?[\s\S]*?\?>/ ]` |
|
|
152
152
|
| `includeAutoGeneratedTags`<br>`--no-include-auto-generated-tags` | Insert elements generated by HTML parser; when `false`, omits auto-generated tags | `true` |
|
|
@@ -223,32 +223,36 @@ const result = await minify(html, {
|
|
|
223
223
|
|
|
224
224
|
## Minification comparison
|
|
225
225
|
|
|
226
|
-
How does HTML Minifier Next compare to other minifiers
|
|
226
|
+
How does HTML Minifier Next compare to other minifiers? (All with the most aggressive settings, though without [hyper-optimization](https://meiert.com/blog/the-ways-of-writing-html/#toc-hyper-optimized). Minimize does not minify CSS or JS.)
|
|
227
227
|
|
|
228
228
|
<!-- Auto-generated benchmarks, don’t edit -->
|
|
229
|
-
| Site | Original Size (KB) | HTML Minifier Next | htmlnano | @swc/html | minify-html | minimize | htmlcompressor.com |
|
|
230
|
-
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
231
|
-
| [A List Apart](https://alistapart.com/) |
|
|
232
|
-
| [Apple](https://www.apple.com/) |
|
|
233
|
-
| [BBC](https://www.bbc.co.uk/) |
|
|
234
|
-
| [
|
|
235
|
-
| [
|
|
236
|
-
| [
|
|
237
|
-
| [
|
|
238
|
-
| [
|
|
239
|
-
| [
|
|
240
|
-
| [
|
|
241
|
-
| [
|
|
242
|
-
| [
|
|
243
|
-
| [Leanpub](https://leanpub.com/) |
|
|
244
|
-
| [Mastodon](https://mastodon.social/explore) | 35 | **26** | 30 | 33 | 33 | 34 | 34 |
|
|
245
|
-
| [MDN](https://developer.mozilla.org/en-US/) | 107 | **62** | 64 | 64 |
|
|
246
|
-
| [Middle East Eye](https://www.middleeasteye.net/) |
|
|
247
|
-
| [
|
|
248
|
-
| [
|
|
249
|
-
| [
|
|
250
|
-
|
|
251
|
-
(
|
|
229
|
+
| Site | Original Size (KB) | [HTML Minifier Next](https://github.com/j9t/html-minifier-next)<br> | [HTML Minifier Terser](https://github.com/terser/html-minifier-terser)<br> | [htmlnano](https://github.com/posthtml/htmlnano)<br> | [@swc/html](https://github.com/swc-project/swc)<br> | [minify-html](https://github.com/wilsonzlin/minify-html)<br> | [minimize](https://github.com/Swaagie/minimize)<br> | [htmlcompressor.com](https://htmlcompressor.com/) |
|
|
230
|
+
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
231
|
+
| [A List Apart](https://alistapart.com/) | 59 | **49** | 50 | 51 | 52 | 51 | 54 | 52 |
|
|
232
|
+
| [Apple](https://www.apple.com/) | 181 | **140** | **140** | 159 | 162 | 163 | 167 | 165 |
|
|
233
|
+
| [BBC](https://www.bbc.co.uk/) | 728 | **662** | 672 | 684 | 684 | 685 | 721 | n/a |
|
|
234
|
+
| [CSS-Tricks](https://css-tricks.com/) | 161 | 121 | **119** | 127 | 142 | 142 | 147 | 144 |
|
|
235
|
+
| [ECMAScript](https://tc39.es/ecma262/) | 7237 | **6341** | **6341** | 6561 | 6444 | 6567 | 6614 | n/a |
|
|
236
|
+
| [EFF](https://www.eff.org/) | 55 | **47** | **47** | 49 | 48 | 49 | 50 | 50 |
|
|
237
|
+
| [FAZ](https://www.faz.net/aktuell/) | 1487 | 1386 | 1391 | **1338** | 1416 | 1426 | 1437 | n/a |
|
|
238
|
+
| [Frontend Dogma](https://frontenddogma.com/) | 221 | **212** | **212** | 233 | 218 | 220 | 238 | 219 |
|
|
239
|
+
| [Google](https://www.google.com/) | 18 | **17** | **17** | **17** | **17** | **17** | 18 | 18 |
|
|
240
|
+
| [Ground News](https://ground.news/) | 2345 | **2060** | 2063 | 2155 | 2176 | 2181 | 2332 | n/a |
|
|
241
|
+
| [HTML Living Standard](https://html.spec.whatwg.org/multipage/) | 149 | **147** | **147** | 153 | **147** | 149 | 155 | 149 |
|
|
242
|
+
| [Igalia](https://www.igalia.com/) | 49 | **33** | **33** | 35 | 35 | 35 | 36 | 36 |
|
|
243
|
+
| [Leanpub](https://leanpub.com/) | 1405 | **1185** | **1185** | 1192 | 1190 | 1187 | 1399 | n/a |
|
|
244
|
+
| [Mastodon](https://mastodon.social/explore) | 35 | **26** | **26** | 30 | 33 | 33 | 34 | 34 |
|
|
245
|
+
| [MDN](https://developer.mozilla.org/en-US/) | 107 | **62** | **62** | 64 | 64 | 65 | 67 | 68 |
|
|
246
|
+
| [Middle East Eye](https://www.middleeasteye.net/) | 222 | **195** | **195** | 202 | 200 | 200 | 202 | 203 |
|
|
247
|
+
| [Nielsen Norman Group](https://www.nngroup.com/) | 84 | 71 | 71 | **53** | 71 | 72 | 74 | 73 |
|
|
248
|
+
| [SitePoint](https://www.sitepoint.com/) | 508 | **366** | **366** | 444 | 482 | 486 | 504 | n/a |
|
|
249
|
+
| [TetraLogical](https://tetralogical.com/) | 44 | 38 | 38 | **35** | 38 | 38 | 39 | 39 |
|
|
250
|
+
| [TPGi](https://www.tpgi.com/) | 99 | **79** | **79** | 84 | 84 | 84 | 88 | 86 |
|
|
251
|
+
| [United Nations](https://www.un.org/en/) | 153 | **114** | 116 | 123 | 127 | 126 | 132 | 125 |
|
|
252
|
+
| [W3C](https://www.w3.org/) | 50 | **36** | **36** | 38 | 38 | 38 | 40 | 38 |
|
|
253
|
+
| **Average processing time** | | 336 ms (22/22) | 374 ms (22/22) | 188 ms (22/22) | 70 ms (22/22) | **17 ms (22/22)** | 366 ms (22/22) | 1351 ms (16/22) |
|
|
254
|
+
|
|
255
|
+
(Last updated: Dec 7, 2025)
|
|
252
256
|
<!-- End auto-generated -->
|
|
253
257
|
|
|
254
258
|
## Examples
|
|
@@ -390,21 +394,23 @@ ignoreCustomFragments: [/\{\{[\s\S]{0,500}?\}\}/]
|
|
|
390
394
|
|
|
391
395
|
## Running HTML Minifier Next
|
|
392
396
|
|
|
397
|
+
### Local server
|
|
398
|
+
|
|
399
|
+
```shell
|
|
400
|
+
npm run serve
|
|
401
|
+
```
|
|
402
|
+
|
|
393
403
|
### Benchmarks
|
|
394
404
|
|
|
395
405
|
Benchmarks for minified HTML:
|
|
396
406
|
|
|
397
407
|
```shell
|
|
398
|
-
cd benchmarks
|
|
399
|
-
npm
|
|
408
|
+
cd benchmarks;
|
|
409
|
+
npm i;
|
|
400
410
|
npm run benchmarks
|
|
401
411
|
```
|
|
402
412
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
```shell
|
|
406
|
-
npm run serve
|
|
407
|
-
```
|
|
413
|
+
(In case of dependency conflicts, run `npm i` with the `--legacy-peer-deps` flag.)
|
|
408
414
|
|
|
409
415
|
## Acknowledgements
|
|
410
416
|
|
package/cli.js
CHANGED
|
@@ -129,7 +129,7 @@ const mainOptions = {
|
|
|
129
129
|
customAttrSurround: ['Arrays of regexes that allow to support custom attribute surround expressions (e.g., “<input {{#if value}}checked="checked"{{/if}}>”)', parseJSONRegExpArray],
|
|
130
130
|
customEventAttributes: ['Arrays of regexes that allow to support custom event attributes for minifyJS (e.g., “ng-click”)', parseJSONRegExpArray],
|
|
131
131
|
decodeEntities: 'Use direct Unicode characters whenever possible',
|
|
132
|
-
html5: 'Don’t parse input according to the HTML specification',
|
|
132
|
+
html5: 'Don’t parse input according to the HTML specification (not recommended for modern HTML)',
|
|
133
133
|
ignoreCustomComments: ['Array of regexes that allow to ignore certain comments, when matched', parseJSONRegExpArray],
|
|
134
134
|
ignoreCustomFragments: ['Array of regexes that allow to ignore certain fragments, when matched (e.g., “<?php … ?>”, “{{ … }}”)', parseJSONRegExpArray],
|
|
135
135
|
includeAutoGeneratedTags: 'Don’t insert elements generated by HTML parser',
|
|
@@ -258,8 +258,10 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
258
258
|
|
|
259
259
|
(async () => {
|
|
260
260
|
let content;
|
|
261
|
+
let filesProvided = false;
|
|
261
262
|
await program.arguments('[files...]').action(function (files) {
|
|
262
263
|
content = files.map(readFile).join('');
|
|
264
|
+
filesProvided = files.length > 0;
|
|
263
265
|
}).parseAsync(process.argv);
|
|
264
266
|
|
|
265
267
|
const programOptions = program.opts();
|
|
@@ -616,7 +618,7 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
616
618
|
console.error(`Total: ${totalOriginal.toLocaleString()} → ${totalMinified.toLocaleString()} bytes (${sign}${Math.abs(totalSaved).toLocaleString()}, ${totalPercentage}%)`);
|
|
617
619
|
}
|
|
618
620
|
})();
|
|
619
|
-
} else if (
|
|
621
|
+
} else if (filesProvided) { // Minifying one or more files specified on the CMD line
|
|
620
622
|
writeMinify();
|
|
621
623
|
} else { // Minifying input coming from STDIN
|
|
622
624
|
content = '';
|
package/dist/htmlminifier.cjs
CHANGED
|
@@ -704,18 +704,88 @@ function getPresetNames() {
|
|
|
704
704
|
return Object.keys(presets);
|
|
705
705
|
}
|
|
706
706
|
|
|
707
|
-
|
|
707
|
+
// Hoisted, reusable RegExp patterns and tiny helpers to avoid repeated allocations in hot paths
|
|
708
|
+
const RE_WS_START = /^[ \n\r\t\f]+/;
|
|
709
|
+
const RE_WS_END = /[ \n\r\t\f]+$/;
|
|
710
|
+
const RE_ALL_WS_NBSP = /[ \n\r\t\f\xA0]+/g;
|
|
711
|
+
const RE_NBSP_LEADING_GROUP = /(^|\xA0+)[^\xA0]+/g;
|
|
712
|
+
const RE_NBSP_LEAD_GROUP = /(\xA0+)[^\xA0]+/g;
|
|
713
|
+
const RE_NBSP_TRAILING_GROUP = /[^\xA0]+(\xA0+)/g;
|
|
714
|
+
const RE_NBSP_TRAILING_STRIP = /[^\xA0]+$/;
|
|
715
|
+
const RE_CONDITIONAL_COMMENT = /^\[if\s[^\]]+]|\[endif]$/;
|
|
716
|
+
const RE_EVENT_ATTR_DEFAULT = /^on[a-z]{3,}$/;
|
|
717
|
+
const RE_CAN_REMOVE_ATTR_QUOTES = /^[^ \t\n\f\r"'`=<>]+$/;
|
|
718
|
+
const RE_TRAILING_SEMICOLON = /;$/;
|
|
719
|
+
const RE_AMP_ENTITY = /&(#?[0-9a-zA-Z]+;)/g;
|
|
720
|
+
|
|
721
|
+
// Tiny stable stringify for options signatures (sorted keys, shallow, nested objects)
|
|
722
|
+
function stableStringify(obj) {
|
|
723
|
+
if (obj == null || typeof obj !== 'object') return JSON.stringify(obj);
|
|
724
|
+
if (Array.isArray(obj)) return '[' + obj.map(stableStringify).join(',') + ']';
|
|
725
|
+
const keys = Object.keys(obj).sort();
|
|
726
|
+
let out = '{';
|
|
727
|
+
for (let i = 0; i < keys.length; i++) {
|
|
728
|
+
const k = keys[i];
|
|
729
|
+
out += JSON.stringify(k) + ':' + stableStringify(obj[k]) + (i < keys.length - 1 ? ',' : '');
|
|
730
|
+
}
|
|
731
|
+
return out + '}';
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Minimal LRU cache for strings and promises
|
|
735
|
+
class LRU {
|
|
736
|
+
constructor(limit = 200) {
|
|
737
|
+
this.limit = limit;
|
|
738
|
+
this.map = new Map();
|
|
739
|
+
}
|
|
740
|
+
get(key) {
|
|
741
|
+
const v = this.map.get(key);
|
|
742
|
+
if (v !== undefined) {
|
|
743
|
+
this.map.delete(key);
|
|
744
|
+
this.map.set(key, v);
|
|
745
|
+
}
|
|
746
|
+
return v;
|
|
747
|
+
}
|
|
748
|
+
set(key, value) {
|
|
749
|
+
if (this.map.has(key)) this.map.delete(key);
|
|
750
|
+
this.map.set(key, value);
|
|
751
|
+
if (this.map.size > this.limit) {
|
|
752
|
+
const first = this.map.keys().next().value;
|
|
753
|
+
this.map.delete(first);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
delete(key) { this.map.delete(key); }
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Per-process caches
|
|
760
|
+
const jsMinifyCache = new LRU(200);
|
|
761
|
+
const cssMinifyCache = new LRU(200);
|
|
762
|
+
|
|
763
|
+
const trimWhitespace = str => {
|
|
764
|
+
if (!str) return str;
|
|
765
|
+
// Fast path: if no whitespace at start or end, return early
|
|
766
|
+
if (!/^[ \n\r\t\f]/.test(str) && !/[ \n\r\t\f]$/.test(str)) {
|
|
767
|
+
return str;
|
|
768
|
+
}
|
|
769
|
+
return str.replace(RE_WS_START, '').replace(RE_WS_END, '');
|
|
770
|
+
};
|
|
708
771
|
|
|
709
772
|
function collapseWhitespaceAll(str) {
|
|
773
|
+
if (!str) return str;
|
|
774
|
+
// Fast path: if there are no common whitespace characters, return early
|
|
775
|
+
if (!/[ \n\r\t\f\xA0]/.test(str)) {
|
|
776
|
+
return str;
|
|
777
|
+
}
|
|
710
778
|
// Non-breaking space is specifically handled inside the replacer function here:
|
|
711
|
-
return str
|
|
712
|
-
return spaces === '\t' ? '\t' : spaces.replace(
|
|
779
|
+
return str.replace(RE_ALL_WS_NBSP, function (spaces) {
|
|
780
|
+
return spaces === '\t' ? '\t' : spaces.replace(RE_NBSP_LEADING_GROUP, '$1 ');
|
|
713
781
|
});
|
|
714
782
|
}
|
|
715
783
|
|
|
716
784
|
function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
717
785
|
let lineBreakBefore = ''; let lineBreakAfter = '';
|
|
718
786
|
|
|
787
|
+
if (!str) return str;
|
|
788
|
+
|
|
719
789
|
if (options.preserveLineBreaks) {
|
|
720
790
|
str = str.replace(/^[ \n\r\t\f]*?[\n\r][ \n\r\t\f]*/, function () {
|
|
721
791
|
lineBreakBefore = '\n';
|
|
@@ -733,7 +803,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
733
803
|
if (conservative && spaces === '\t') {
|
|
734
804
|
return '\t';
|
|
735
805
|
}
|
|
736
|
-
return spaces.replace(/^[^\xA0]+/, '').replace(
|
|
806
|
+
return spaces.replace(/^[^\xA0]+/, '').replace(RE_NBSP_LEAD_GROUP, '$1 ') || (conservative ? ' ' : '');
|
|
737
807
|
});
|
|
738
808
|
}
|
|
739
809
|
|
|
@@ -744,7 +814,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
744
814
|
if (conservative && spaces === '\t') {
|
|
745
815
|
return '\t';
|
|
746
816
|
}
|
|
747
|
-
return spaces.replace(
|
|
817
|
+
return spaces.replace(RE_NBSP_TRAILING_GROUP, ' $1').replace(RE_NBSP_TRAILING_STRIP, '') || (conservative ? ' ' : '');
|
|
748
818
|
});
|
|
749
819
|
}
|
|
750
820
|
|
|
@@ -776,7 +846,7 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements,
|
|
|
776
846
|
}
|
|
777
847
|
|
|
778
848
|
function isConditionalComment(text) {
|
|
779
|
-
return
|
|
849
|
+
return RE_CONDITIONAL_COMMENT.test(text);
|
|
780
850
|
}
|
|
781
851
|
|
|
782
852
|
function isIgnoredComment(text, options) {
|
|
@@ -798,12 +868,12 @@ function isEventAttribute(attrName, options) {
|
|
|
798
868
|
}
|
|
799
869
|
return false;
|
|
800
870
|
}
|
|
801
|
-
return
|
|
871
|
+
return RE_EVENT_ATTR_DEFAULT.test(attrName);
|
|
802
872
|
}
|
|
803
873
|
|
|
804
874
|
function canRemoveAttributeQuotes(value) {
|
|
805
875
|
// https://mathiasbynens.be/notes/unquoted-attribute-values
|
|
806
|
-
return
|
|
876
|
+
return RE_CAN_REMOVE_ATTR_QUOTES.test(value);
|
|
807
877
|
}
|
|
808
878
|
|
|
809
879
|
function attributesInclude(attributes, attribute) {
|
|
@@ -1014,7 +1084,7 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
1014
1084
|
} else if (attrName === 'style') {
|
|
1015
1085
|
attrValue = trimWhitespace(attrValue);
|
|
1016
1086
|
if (attrValue) {
|
|
1017
|
-
if (
|
|
1087
|
+
if (attrValue.endsWith(';') && !/&#?[0-9a-zA-Z]+;$/.test(attrValue)) {
|
|
1018
1088
|
attrValue = attrValue.replace(/\s*;$/, ';');
|
|
1019
1089
|
}
|
|
1020
1090
|
attrValue = await options.minifyCSS(attrValue, 'inline');
|
|
@@ -1333,7 +1403,10 @@ async function normalizeAttr(attr, attrs, tag, options) {
|
|
|
1333
1403
|
let attrValue = attr.value;
|
|
1334
1404
|
|
|
1335
1405
|
if (options.decodeEntities && attrValue) {
|
|
1336
|
-
|
|
1406
|
+
// Fast path: only decode when entities are present
|
|
1407
|
+
if (attrValue.indexOf('&') !== -1) {
|
|
1408
|
+
attrValue = entities.decodeHTMLStrict(attrValue);
|
|
1409
|
+
}
|
|
1337
1410
|
}
|
|
1338
1411
|
|
|
1339
1412
|
if ((options.removeRedundantAttributes &&
|
|
@@ -1354,8 +1427,8 @@ async function normalizeAttr(attr, attrs, tag, options) {
|
|
|
1354
1427
|
return;
|
|
1355
1428
|
}
|
|
1356
1429
|
|
|
1357
|
-
if (options.decodeEntities && attrValue) {
|
|
1358
|
-
attrValue = attrValue.replace(
|
|
1430
|
+
if (options.decodeEntities && attrValue && attrValue.indexOf('&') !== -1) {
|
|
1431
|
+
attrValue = attrValue.replace(RE_AMP_ENTITY, '&$1');
|
|
1359
1432
|
}
|
|
1360
1433
|
|
|
1361
1434
|
return {
|
|
@@ -1475,6 +1548,10 @@ const processOptions = (inputOptions) => {
|
|
|
1475
1548
|
const lightningCssOptions = typeof option === 'object' ? option : {};
|
|
1476
1549
|
|
|
1477
1550
|
options.minifyCSS = async function (text, type) {
|
|
1551
|
+
// Fast path: nothing to minify
|
|
1552
|
+
if (!text || !text.trim()) {
|
|
1553
|
+
return text;
|
|
1554
|
+
}
|
|
1478
1555
|
text = await replaceAsync(
|
|
1479
1556
|
text,
|
|
1480
1557
|
/(url\s*\(\s*)(?:"([^"]*)"|'([^']*)'|([^\s)]+))(\s*\))/ig,
|
|
@@ -1493,10 +1570,20 @@ const processOptions = (inputOptions) => {
|
|
|
1493
1570
|
}
|
|
1494
1571
|
}
|
|
1495
1572
|
);
|
|
1496
|
-
|
|
1573
|
+
// Cache key: wrapped content, type, options signature
|
|
1497
1574
|
const inputCSS = wrapCSS(text, type);
|
|
1575
|
+
const cssSig = stableStringify({ type, opts: lightningCssOptions, cont: !!options.continueOnMinifyError });
|
|
1576
|
+
// For large inputs, use length and content fingerprint (first/last 50 chars) to prevent collisions
|
|
1577
|
+
const cssKey = inputCSS.length > 2048
|
|
1578
|
+
? (inputCSS.length + '|' + inputCSS.slice(0, 50) + inputCSS.slice(-50) + '|' + type + '|' + cssSig)
|
|
1579
|
+
: (inputCSS + '|' + type + '|' + cssSig);
|
|
1498
1580
|
|
|
1499
1581
|
try {
|
|
1582
|
+
const cached = cssMinifyCache.get(cssKey);
|
|
1583
|
+
if (cached) {
|
|
1584
|
+
return cached;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1500
1587
|
const result = lightningcss.transform({
|
|
1501
1588
|
filename: 'input.css',
|
|
1502
1589
|
code: Buffer.from(inputCSS),
|
|
@@ -1519,12 +1606,12 @@ const processOptions = (inputOptions) => {
|
|
|
1519
1606
|
|
|
1520
1607
|
// Preserve if output is empty and input had template syntax or UIDs
|
|
1521
1608
|
// This catches cases where Lightning CSS removed content that should be preserved
|
|
1522
|
-
|
|
1523
|
-
return text;
|
|
1524
|
-
}
|
|
1609
|
+
const finalOutput = (text.trim() && !outputCSS.trim() && (looksLikeTemplate || hasUID)) ? text : outputCSS;
|
|
1525
1610
|
|
|
1526
|
-
|
|
1611
|
+
cssMinifyCache.set(cssKey, finalOutput);
|
|
1612
|
+
return finalOutput;
|
|
1527
1613
|
} catch (err) {
|
|
1614
|
+
cssMinifyCache.delete(cssKey);
|
|
1528
1615
|
if (!options.continueOnMinifyError) {
|
|
1529
1616
|
throw err;
|
|
1530
1617
|
}
|
|
@@ -1550,10 +1637,39 @@ const processOptions = (inputOptions) => {
|
|
|
1550
1637
|
|
|
1551
1638
|
terserOptions.parse.bare_returns = inline;
|
|
1552
1639
|
|
|
1640
|
+
let jsKey;
|
|
1553
1641
|
try {
|
|
1554
|
-
|
|
1555
|
-
|
|
1642
|
+
// Fast path: avoid invoking Terser for empty/whitespace-only content
|
|
1643
|
+
if (!code || !code.trim()) {
|
|
1644
|
+
return '';
|
|
1645
|
+
}
|
|
1646
|
+
// Cache key: content, inline, options signature (subset)
|
|
1647
|
+
const terserSig = stableStringify({
|
|
1648
|
+
compress: terserOptions.compress,
|
|
1649
|
+
mangle: terserOptions.mangle,
|
|
1650
|
+
ecma: terserOptions.ecma,
|
|
1651
|
+
toplevel: terserOptions.toplevel,
|
|
1652
|
+
module: terserOptions.module,
|
|
1653
|
+
keep_fnames: terserOptions.keep_fnames,
|
|
1654
|
+
format: terserOptions.format,
|
|
1655
|
+
cont: !!options.continueOnMinifyError,
|
|
1656
|
+
});
|
|
1657
|
+
// For large inputs, use length and content fingerprint (first/last 50 chars) to prevent collisions
|
|
1658
|
+
jsKey = (code.length > 2048 ? (code.length + '|' + code.slice(0, 50) + code.slice(-50) + '|') : (code + '|')) + (inline ? '1' : '0') + '|' + terserSig;
|
|
1659
|
+
const cached = jsMinifyCache.get(jsKey);
|
|
1660
|
+
if (cached) {
|
|
1661
|
+
return await cached;
|
|
1662
|
+
}
|
|
1663
|
+
const inFlight = (async () => {
|
|
1664
|
+
const result = await terser.minify(code, terserOptions);
|
|
1665
|
+
return result.code.replace(RE_TRAILING_SEMICOLON, '');
|
|
1666
|
+
})();
|
|
1667
|
+
jsMinifyCache.set(jsKey, inFlight);
|
|
1668
|
+
const resolved = await inFlight;
|
|
1669
|
+
jsMinifyCache.set(jsKey, resolved);
|
|
1670
|
+
return resolved;
|
|
1556
1671
|
} catch (err) {
|
|
1672
|
+
if (jsKey) jsMinifyCache.delete(jsKey);
|
|
1557
1673
|
if (!options.continueOnMinifyError) {
|
|
1558
1674
|
throw err;
|
|
1559
1675
|
}
|
|
@@ -2015,7 +2131,9 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2015
2131
|
prevTag = prevTag === '' ? 'comment' : prevTag;
|
|
2016
2132
|
nextTag = nextTag === '' ? 'comment' : nextTag;
|
|
2017
2133
|
if (options.decodeEntities && text && !specialContentTags.has(currentTag)) {
|
|
2018
|
-
text
|
|
2134
|
+
if (text.indexOf('&') !== -1) {
|
|
2135
|
+
text = entities.decodeHTML(text);
|
|
2136
|
+
}
|
|
2019
2137
|
}
|
|
2020
2138
|
if (options.collapseWhitespace) {
|
|
2021
2139
|
if (!stackNoTrimWhitespace.length) {
|
|
@@ -2089,11 +2207,16 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2089
2207
|
charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
|
|
2090
2208
|
if (options.decodeEntities && text && !specialContentTags.has(currentTag)) {
|
|
2091
2209
|
// Escape any `&` symbols that start either:
|
|
2092
|
-
// 1) a legacy named character reference (i.e
|
|
2093
|
-
// 2) or any other character reference (i.e
|
|
2210
|
+
// 1) a legacy named character reference (i.e., one that doesn’t end with `;`)
|
|
2211
|
+
// 2) or any other character reference (i.e., one that does end with `;`)
|
|
2094
2212
|
// Note that `&` can be escaped as `&`, without the semi-colon.
|
|
2095
2213
|
// https://mathiasbynens.be/notes/ambiguous-ampersands
|
|
2096
|
-
|
|
2214
|
+
if (text.indexOf('&') !== -1) {
|
|
2215
|
+
text = text.replace(/&((?:Iacute|aacute|uacute|plusmn|Otilde|otilde|agrave|Agrave|Yacute|yacute|Oslash|oslash|atilde|Atilde|brvbar|ccedil|Ccedil|Ograve|curren|divide|eacute|Eacute|ograve|Oacute|egrave|Egrave|Ugrave|frac12|frac14|frac34|ugrave|oacute|iacute|Ntilde|ntilde|Uacute|middot|igrave|Igrave|iquest|Aacute|cedil|laquo|micro|iexcl|Icirc|icirc|acirc|Ucirc|Ecirc|ocirc|Ocirc|ecirc|ucirc|Aring|aring|AElig|aelig|acute|pound|raquo|Acirc|times|THORN|szlig|thorn|COPY|auml|ordf|ordm|Uuml|macr|uuml|Auml|ouml|Ouml|para|nbsp|euml|quot|QUOT|Euml|yuml|cent|sect|copy|sup1|sup2|sup3|iuml|Iuml|ETH|shy|reg|not|yen|amp|AMP|REG|uml|eth|deg|gt|GT|LT|lt)(?!;)|(?:#?[0-9a-zA-Z]+;))/g, '&$1');
|
|
2216
|
+
}
|
|
2217
|
+
if (text.indexOf('<') !== -1) {
|
|
2218
|
+
text = text.replace(/</g, '<');
|
|
2219
|
+
}
|
|
2097
2220
|
}
|
|
2098
2221
|
if (uidPattern && options.collapseWhitespace && stackNoTrimWhitespace.length) {
|
|
2099
2222
|
text = text.replace(uidPattern, function (match, prefix, index) {
|
|
@@ -39757,18 +39757,88 @@ function getPresetNames() {
|
|
|
39757
39757
|
return Object.keys(presets);
|
|
39758
39758
|
}
|
|
39759
39759
|
|
|
39760
|
-
|
|
39760
|
+
// Hoisted, reusable RegExp patterns and tiny helpers to avoid repeated allocations in hot paths
|
|
39761
|
+
const RE_WS_START = /^[ \n\r\t\f]+/;
|
|
39762
|
+
const RE_WS_END = /[ \n\r\t\f]+$/;
|
|
39763
|
+
const RE_ALL_WS_NBSP = /[ \n\r\t\f\xA0]+/g;
|
|
39764
|
+
const RE_NBSP_LEADING_GROUP = /(^|\xA0+)[^\xA0]+/g;
|
|
39765
|
+
const RE_NBSP_LEAD_GROUP = /(\xA0+)[^\xA0]+/g;
|
|
39766
|
+
const RE_NBSP_TRAILING_GROUP = /[^\xA0]+(\xA0+)/g;
|
|
39767
|
+
const RE_NBSP_TRAILING_STRIP = /[^\xA0]+$/;
|
|
39768
|
+
const RE_CONDITIONAL_COMMENT = /^\[if\s[^\]]+]|\[endif]$/;
|
|
39769
|
+
const RE_EVENT_ATTR_DEFAULT = /^on[a-z]{3,}$/;
|
|
39770
|
+
const RE_CAN_REMOVE_ATTR_QUOTES = /^[^ \t\n\f\r"'`=<>]+$/;
|
|
39771
|
+
const RE_TRAILING_SEMICOLON = /;$/;
|
|
39772
|
+
const RE_AMP_ENTITY = /&(#?[0-9a-zA-Z]+;)/g;
|
|
39773
|
+
|
|
39774
|
+
// Tiny stable stringify for options signatures (sorted keys, shallow, nested objects)
|
|
39775
|
+
function stableStringify(obj) {
|
|
39776
|
+
if (obj == null || typeof obj !== 'object') return JSON.stringify(obj);
|
|
39777
|
+
if (Array.isArray(obj)) return '[' + obj.map(stableStringify).join(',') + ']';
|
|
39778
|
+
const keys = Object.keys(obj).sort();
|
|
39779
|
+
let out = '{';
|
|
39780
|
+
for (let i = 0; i < keys.length; i++) {
|
|
39781
|
+
const k = keys[i];
|
|
39782
|
+
out += JSON.stringify(k) + ':' + stableStringify(obj[k]) + (i < keys.length - 1 ? ',' : '');
|
|
39783
|
+
}
|
|
39784
|
+
return out + '}';
|
|
39785
|
+
}
|
|
39786
|
+
|
|
39787
|
+
// Minimal LRU cache for strings and promises
|
|
39788
|
+
class LRU {
|
|
39789
|
+
constructor(limit = 200) {
|
|
39790
|
+
this.limit = limit;
|
|
39791
|
+
this.map = new Map();
|
|
39792
|
+
}
|
|
39793
|
+
get(key) {
|
|
39794
|
+
const v = this.map.get(key);
|
|
39795
|
+
if (v !== undefined) {
|
|
39796
|
+
this.map.delete(key);
|
|
39797
|
+
this.map.set(key, v);
|
|
39798
|
+
}
|
|
39799
|
+
return v;
|
|
39800
|
+
}
|
|
39801
|
+
set(key, value) {
|
|
39802
|
+
if (this.map.has(key)) this.map.delete(key);
|
|
39803
|
+
this.map.set(key, value);
|
|
39804
|
+
if (this.map.size > this.limit) {
|
|
39805
|
+
const first = this.map.keys().next().value;
|
|
39806
|
+
this.map.delete(first);
|
|
39807
|
+
}
|
|
39808
|
+
}
|
|
39809
|
+
delete(key) { this.map.delete(key); }
|
|
39810
|
+
}
|
|
39811
|
+
|
|
39812
|
+
// Per-process caches
|
|
39813
|
+
const jsMinifyCache = new LRU(200);
|
|
39814
|
+
const cssMinifyCache = new LRU(200);
|
|
39815
|
+
|
|
39816
|
+
const trimWhitespace = str => {
|
|
39817
|
+
if (!str) return str;
|
|
39818
|
+
// Fast path: if no whitespace at start or end, return early
|
|
39819
|
+
if (!/^[ \n\r\t\f]/.test(str) && !/[ \n\r\t\f]$/.test(str)) {
|
|
39820
|
+
return str;
|
|
39821
|
+
}
|
|
39822
|
+
return str.replace(RE_WS_START, '').replace(RE_WS_END, '');
|
|
39823
|
+
};
|
|
39761
39824
|
|
|
39762
39825
|
function collapseWhitespaceAll(str) {
|
|
39826
|
+
if (!str) return str;
|
|
39827
|
+
// Fast path: if there are no common whitespace characters, return early
|
|
39828
|
+
if (!/[ \n\r\t\f\xA0]/.test(str)) {
|
|
39829
|
+
return str;
|
|
39830
|
+
}
|
|
39763
39831
|
// Non-breaking space is specifically handled inside the replacer function here:
|
|
39764
|
-
return str
|
|
39765
|
-
return spaces === '\t' ? '\t' : spaces.replace(
|
|
39832
|
+
return str.replace(RE_ALL_WS_NBSP, function (spaces) {
|
|
39833
|
+
return spaces === '\t' ? '\t' : spaces.replace(RE_NBSP_LEADING_GROUP, '$1 ');
|
|
39766
39834
|
});
|
|
39767
39835
|
}
|
|
39768
39836
|
|
|
39769
39837
|
function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
39770
39838
|
let lineBreakBefore = ''; let lineBreakAfter = '';
|
|
39771
39839
|
|
|
39840
|
+
if (!str) return str;
|
|
39841
|
+
|
|
39772
39842
|
if (options.preserveLineBreaks) {
|
|
39773
39843
|
str = str.replace(/^[ \n\r\t\f]*?[\n\r][ \n\r\t\f]*/, function () {
|
|
39774
39844
|
lineBreakBefore = '\n';
|
|
@@ -39786,7 +39856,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
39786
39856
|
if (conservative && spaces === '\t') {
|
|
39787
39857
|
return '\t';
|
|
39788
39858
|
}
|
|
39789
|
-
return spaces.replace(/^[^\xA0]+/, '').replace(
|
|
39859
|
+
return spaces.replace(/^[^\xA0]+/, '').replace(RE_NBSP_LEAD_GROUP, '$1 ') || (conservative ? ' ' : '');
|
|
39790
39860
|
});
|
|
39791
39861
|
}
|
|
39792
39862
|
|
|
@@ -39797,7 +39867,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
39797
39867
|
if (conservative && spaces === '\t') {
|
|
39798
39868
|
return '\t';
|
|
39799
39869
|
}
|
|
39800
|
-
return spaces.replace(
|
|
39870
|
+
return spaces.replace(RE_NBSP_TRAILING_GROUP, ' $1').replace(RE_NBSP_TRAILING_STRIP, '') || (conservative ? ' ' : '');
|
|
39801
39871
|
});
|
|
39802
39872
|
}
|
|
39803
39873
|
|
|
@@ -39829,7 +39899,7 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements,
|
|
|
39829
39899
|
}
|
|
39830
39900
|
|
|
39831
39901
|
function isConditionalComment(text) {
|
|
39832
|
-
return
|
|
39902
|
+
return RE_CONDITIONAL_COMMENT.test(text);
|
|
39833
39903
|
}
|
|
39834
39904
|
|
|
39835
39905
|
function isIgnoredComment(text, options) {
|
|
@@ -39851,12 +39921,12 @@ function isEventAttribute(attrName, options) {
|
|
|
39851
39921
|
}
|
|
39852
39922
|
return false;
|
|
39853
39923
|
}
|
|
39854
|
-
return
|
|
39924
|
+
return RE_EVENT_ATTR_DEFAULT.test(attrName);
|
|
39855
39925
|
}
|
|
39856
39926
|
|
|
39857
39927
|
function canRemoveAttributeQuotes(value) {
|
|
39858
39928
|
// https://mathiasbynens.be/notes/unquoted-attribute-values
|
|
39859
|
-
return
|
|
39929
|
+
return RE_CAN_REMOVE_ATTR_QUOTES.test(value);
|
|
39860
39930
|
}
|
|
39861
39931
|
|
|
39862
39932
|
function attributesInclude(attributes, attribute) {
|
|
@@ -40067,7 +40137,7 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
40067
40137
|
} else if (attrName === 'style') {
|
|
40068
40138
|
attrValue = trimWhitespace(attrValue);
|
|
40069
40139
|
if (attrValue) {
|
|
40070
|
-
if (
|
|
40140
|
+
if (attrValue.endsWith(';') && !/&#?[0-9a-zA-Z]+;$/.test(attrValue)) {
|
|
40071
40141
|
attrValue = attrValue.replace(/\s*;$/, ';');
|
|
40072
40142
|
}
|
|
40073
40143
|
attrValue = await options.minifyCSS(attrValue, 'inline');
|
|
@@ -40386,7 +40456,10 @@ async function normalizeAttr(attr, attrs, tag, options) {
|
|
|
40386
40456
|
let attrValue = attr.value;
|
|
40387
40457
|
|
|
40388
40458
|
if (options.decodeEntities && attrValue) {
|
|
40389
|
-
|
|
40459
|
+
// Fast path: only decode when entities are present
|
|
40460
|
+
if (attrValue.indexOf('&') !== -1) {
|
|
40461
|
+
attrValue = decodeHTMLStrict(attrValue);
|
|
40462
|
+
}
|
|
40390
40463
|
}
|
|
40391
40464
|
|
|
40392
40465
|
if ((options.removeRedundantAttributes &&
|
|
@@ -40407,8 +40480,8 @@ async function normalizeAttr(attr, attrs, tag, options) {
|
|
|
40407
40480
|
return;
|
|
40408
40481
|
}
|
|
40409
40482
|
|
|
40410
|
-
if (options.decodeEntities && attrValue) {
|
|
40411
|
-
attrValue = attrValue.replace(
|
|
40483
|
+
if (options.decodeEntities && attrValue && attrValue.indexOf('&') !== -1) {
|
|
40484
|
+
attrValue = attrValue.replace(RE_AMP_ENTITY, '&$1');
|
|
40412
40485
|
}
|
|
40413
40486
|
|
|
40414
40487
|
return {
|
|
@@ -40528,6 +40601,10 @@ const processOptions = (inputOptions) => {
|
|
|
40528
40601
|
const lightningCssOptions = typeof option === 'object' ? option : {};
|
|
40529
40602
|
|
|
40530
40603
|
options.minifyCSS = async function (text, type) {
|
|
40604
|
+
// Fast path: nothing to minify
|
|
40605
|
+
if (!text || !text.trim()) {
|
|
40606
|
+
return text;
|
|
40607
|
+
}
|
|
40531
40608
|
text = await replaceAsync(
|
|
40532
40609
|
text,
|
|
40533
40610
|
/(url\s*\(\s*)(?:"([^"]*)"|'([^']*)'|([^\s)]+))(\s*\))/ig,
|
|
@@ -40546,10 +40623,20 @@ const processOptions = (inputOptions) => {
|
|
|
40546
40623
|
}
|
|
40547
40624
|
}
|
|
40548
40625
|
);
|
|
40549
|
-
|
|
40626
|
+
// Cache key: wrapped content, type, options signature
|
|
40550
40627
|
const inputCSS = wrapCSS(text, type);
|
|
40628
|
+
const cssSig = stableStringify({ type, opts: lightningCssOptions, cont: !!options.continueOnMinifyError });
|
|
40629
|
+
// For large inputs, use length and content fingerprint (first/last 50 chars) to prevent collisions
|
|
40630
|
+
const cssKey = inputCSS.length > 2048
|
|
40631
|
+
? (inputCSS.length + '|' + inputCSS.slice(0, 50) + inputCSS.slice(-50) + '|' + type + '|' + cssSig)
|
|
40632
|
+
: (inputCSS + '|' + type + '|' + cssSig);
|
|
40551
40633
|
|
|
40552
40634
|
try {
|
|
40635
|
+
const cached = cssMinifyCache.get(cssKey);
|
|
40636
|
+
if (cached) {
|
|
40637
|
+
return cached;
|
|
40638
|
+
}
|
|
40639
|
+
|
|
40553
40640
|
const result = transform({
|
|
40554
40641
|
filename: 'input.css',
|
|
40555
40642
|
code: Buffer.from(inputCSS),
|
|
@@ -40572,12 +40659,12 @@ const processOptions = (inputOptions) => {
|
|
|
40572
40659
|
|
|
40573
40660
|
// Preserve if output is empty and input had template syntax or UIDs
|
|
40574
40661
|
// This catches cases where Lightning CSS removed content that should be preserved
|
|
40575
|
-
|
|
40576
|
-
return text;
|
|
40577
|
-
}
|
|
40662
|
+
const finalOutput = (text.trim() && !outputCSS.trim() && (looksLikeTemplate || hasUID)) ? text : outputCSS;
|
|
40578
40663
|
|
|
40579
|
-
|
|
40664
|
+
cssMinifyCache.set(cssKey, finalOutput);
|
|
40665
|
+
return finalOutput;
|
|
40580
40666
|
} catch (err) {
|
|
40667
|
+
cssMinifyCache.delete(cssKey);
|
|
40581
40668
|
if (!options.continueOnMinifyError) {
|
|
40582
40669
|
throw err;
|
|
40583
40670
|
}
|
|
@@ -40603,10 +40690,39 @@ const processOptions = (inputOptions) => {
|
|
|
40603
40690
|
|
|
40604
40691
|
terserOptions.parse.bare_returns = inline;
|
|
40605
40692
|
|
|
40693
|
+
let jsKey;
|
|
40606
40694
|
try {
|
|
40607
|
-
|
|
40608
|
-
|
|
40695
|
+
// Fast path: avoid invoking Terser for empty/whitespace-only content
|
|
40696
|
+
if (!code || !code.trim()) {
|
|
40697
|
+
return '';
|
|
40698
|
+
}
|
|
40699
|
+
// Cache key: content, inline, options signature (subset)
|
|
40700
|
+
const terserSig = stableStringify({
|
|
40701
|
+
compress: terserOptions.compress,
|
|
40702
|
+
mangle: terserOptions.mangle,
|
|
40703
|
+
ecma: terserOptions.ecma,
|
|
40704
|
+
toplevel: terserOptions.toplevel,
|
|
40705
|
+
module: terserOptions.module,
|
|
40706
|
+
keep_fnames: terserOptions.keep_fnames,
|
|
40707
|
+
format: terserOptions.format,
|
|
40708
|
+
cont: !!options.continueOnMinifyError,
|
|
40709
|
+
});
|
|
40710
|
+
// For large inputs, use length and content fingerprint (first/last 50 chars) to prevent collisions
|
|
40711
|
+
jsKey = (code.length > 2048 ? (code.length + '|' + code.slice(0, 50) + code.slice(-50) + '|') : (code + '|')) + (inline ? '1' : '0') + '|' + terserSig;
|
|
40712
|
+
const cached = jsMinifyCache.get(jsKey);
|
|
40713
|
+
if (cached) {
|
|
40714
|
+
return await cached;
|
|
40715
|
+
}
|
|
40716
|
+
const inFlight = (async () => {
|
|
40717
|
+
const result = await minify$1(code, terserOptions);
|
|
40718
|
+
return result.code.replace(RE_TRAILING_SEMICOLON, '');
|
|
40719
|
+
})();
|
|
40720
|
+
jsMinifyCache.set(jsKey, inFlight);
|
|
40721
|
+
const resolved = await inFlight;
|
|
40722
|
+
jsMinifyCache.set(jsKey, resolved);
|
|
40723
|
+
return resolved;
|
|
40609
40724
|
} catch (err) {
|
|
40725
|
+
if (jsKey) jsMinifyCache.delete(jsKey);
|
|
40610
40726
|
if (!options.continueOnMinifyError) {
|
|
40611
40727
|
throw err;
|
|
40612
40728
|
}
|
|
@@ -41068,7 +41184,9 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
41068
41184
|
prevTag = prevTag === '' ? 'comment' : prevTag;
|
|
41069
41185
|
nextTag = nextTag === '' ? 'comment' : nextTag;
|
|
41070
41186
|
if (options.decodeEntities && text && !specialContentTags.has(currentTag)) {
|
|
41071
|
-
|
|
41187
|
+
if (text.indexOf('&') !== -1) {
|
|
41188
|
+
text = decodeHTML(text);
|
|
41189
|
+
}
|
|
41072
41190
|
}
|
|
41073
41191
|
if (options.collapseWhitespace) {
|
|
41074
41192
|
if (!stackNoTrimWhitespace.length) {
|
|
@@ -41142,11 +41260,16 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
41142
41260
|
charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
|
|
41143
41261
|
if (options.decodeEntities && text && !specialContentTags.has(currentTag)) {
|
|
41144
41262
|
// Escape any `&` symbols that start either:
|
|
41145
|
-
// 1) a legacy named character reference (i.e
|
|
41146
|
-
// 2) or any other character reference (i.e
|
|
41263
|
+
// 1) a legacy named character reference (i.e., one that doesn’t end with `;`)
|
|
41264
|
+
// 2) or any other character reference (i.e., one that does end with `;`)
|
|
41147
41265
|
// Note that `&` can be escaped as `&`, without the semi-colon.
|
|
41148
41266
|
// https://mathiasbynens.be/notes/ambiguous-ampersands
|
|
41149
|
-
|
|
41267
|
+
if (text.indexOf('&') !== -1) {
|
|
41268
|
+
text = text.replace(/&((?:Iacute|aacute|uacute|plusmn|Otilde|otilde|agrave|Agrave|Yacute|yacute|Oslash|oslash|atilde|Atilde|brvbar|ccedil|Ccedil|Ograve|curren|divide|eacute|Eacute|ograve|Oacute|egrave|Egrave|Ugrave|frac12|frac14|frac34|ugrave|oacute|iacute|Ntilde|ntilde|Uacute|middot|igrave|Igrave|iquest|Aacute|cedil|laquo|micro|iexcl|Icirc|icirc|acirc|Ucirc|Ecirc|ocirc|Ocirc|ecirc|ucirc|Aring|aring|AElig|aelig|acute|pound|raquo|Acirc|times|THORN|szlig|thorn|COPY|auml|ordf|ordm|Uuml|macr|uuml|Auml|ouml|Ouml|para|nbsp|euml|quot|QUOT|Euml|yuml|cent|sect|copy|sup1|sup2|sup3|iuml|Iuml|ETH|shy|reg|not|yen|amp|AMP|REG|uml|eth|deg|gt|GT|LT|lt)(?!;)|(?:#?[0-9a-zA-Z]+;))/g, '&$1');
|
|
41269
|
+
}
|
|
41270
|
+
if (text.indexOf('<') !== -1) {
|
|
41271
|
+
text = text.replace(/</g, '<');
|
|
41272
|
+
}
|
|
41150
41273
|
}
|
|
41151
41274
|
if (uidPattern && options.collapseWhitespace && stackNoTrimWhitespace.length) {
|
|
41152
41275
|
text = text.replace(uidPattern, function (match, prefix, index) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"AAknDO,8BAJI,MAAM,YACN,eAAe,GACb,OAAO,CAAC,MAAM,CAAC,CAQ3B;;;;;;;;;;;;UAUS,MAAM;YACN,MAAM;YACN,MAAM;mBACN,MAAM;iBACN,MAAM;kBACN,MAAM;;;;;;;;;;;;;4BAQN,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,qBAAqB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,KAAK,OAAO;;;;;;;wBAMjG,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,SAAS,EAAE,iBAAiB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,KAAK,OAAO;;;;;;;;oBAMhH,OAAO;;;;;;;;gCAOP,OAAO;;;;;;;;kCAOP,OAAO;;;;;;;;yBAOP,OAAO;;;;;;;;2BAOP,OAAO;;;;;;;;4BAOP,OAAO;;;;;;;2BAOP,OAAO;;;;;;;;uBAMP,MAAM,EAAE;;;;;;yBAOR,MAAM;;;;;;yBAKN,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE;;;;;;;4BAKlB,MAAM,EAAE;;;;;;;oCAMR,MAAM;;;;;;;qBAMN,OAAO;;;;;;;YAMP,OAAO;;;;;;;;2BAMP,MAAM,EAAE;;;;;;;;;4BAOR,MAAM,EAAE;;;;;;;+BAQR,OAAO;;;;;;;2BAMP,SAAS,CAAC,MAAM,CAAC;;;;;;uBAMjB,OAAO;;;;;;;;UAKP,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI;;;;;;;;qBAO1B,MAAM;;;;;;;oBAON,MAAM;;;;;;;;;;gBAMN,OAAO,GAAG,OAAO,CAAC,OAAO,cAAc,EAAE,gBAAgB,CAAC,OAAO,cAAc,EAAE,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;;;;;;;;;;eAS9J,OAAO,GAAG,OAAO,QAAQ,EAAE,aAAa,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;;;;;;;;;;iBASzG,OAAO,GAAG,MAAM,GAAG,OAAO,WAAW,EAAE,OAAO,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;;;;;;;;WAS7F,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM;;;;;;;+BAOxB,OAAO;;;;;;;;;;oBAMP,OAAO;;;;;;;;yBASP,OAAO;;;;;;;gCAOP,OAAO;;;;;;;;iCAMP,OAAO;;;;;;;;;;qBAOP,MAAM,EAAE;;;;;;;qBASR,IAAI,GAAG,GAAG;;;;;;;4BAMV,OAAO;;;;;;;;qBAMP,OAAO;;;;;;;;;4BAOP,OAAO,GAAG,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;;;;;;;;0BAQtD,OAAO;;;;;;;;yBAOP,OAAO;;;;;;;;gCAOP,OAAO;;;;;;;iCAOP,OAAO;;;;;;;oCAMP,OAAO;;;;;;;;;;0BAMP,OAAO;;;;;;;;;qBASP,OAAO,GAAG,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,IAAI,CAAC;;;;;;;;;oBAQzD,OAAO,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;;;;;;;;0BAQrC,OAAO;;;;;;;sBAOP,OAAO;;wBA38DkC,cAAc;0BAAd,cAAc;+BAAd,cAAc"}
|
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"relateurl": "^0.2.7",
|
|
14
14
|
"terser": "^5.44.1"
|
|
15
15
|
},
|
|
16
|
-
"description": "Highly configurable, well-tested, JavaScript-based HTML minifier",
|
|
16
|
+
"description": "Highly configurable, well-tested, JavaScript-based HTML minifier (enhanced successor of HTML Minifier)",
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@commitlint/cli": "^20.1.0",
|
|
19
19
|
"@eslint/js": "^9.39.1",
|
|
@@ -84,5 +84,5 @@
|
|
|
84
84
|
"test:watch": "node --test --watch tests/*.spec.js"
|
|
85
85
|
},
|
|
86
86
|
"type": "module",
|
|
87
|
-
"version": "4.
|
|
87
|
+
"version": "4.7.1"
|
|
88
88
|
}
|
package/src/htmlminifier.js
CHANGED
|
@@ -7,18 +7,88 @@ import TokenChain from './tokenchain.js';
|
|
|
7
7
|
import { replaceAsync } from './utils.js';
|
|
8
8
|
import { presets, getPreset, getPresetNames } from './presets.js';
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// Hoisted, reusable RegExp patterns and tiny helpers to avoid repeated allocations in hot paths
|
|
11
|
+
const RE_WS_START = /^[ \n\r\t\f]+/;
|
|
12
|
+
const RE_WS_END = /[ \n\r\t\f]+$/;
|
|
13
|
+
const RE_ALL_WS_NBSP = /[ \n\r\t\f\xA0]+/g;
|
|
14
|
+
const RE_NBSP_LEADING_GROUP = /(^|\xA0+)[^\xA0]+/g;
|
|
15
|
+
const RE_NBSP_LEAD_GROUP = /(\xA0+)[^\xA0]+/g;
|
|
16
|
+
const RE_NBSP_TRAILING_GROUP = /[^\xA0]+(\xA0+)/g;
|
|
17
|
+
const RE_NBSP_TRAILING_STRIP = /[^\xA0]+$/;
|
|
18
|
+
const RE_CONDITIONAL_COMMENT = /^\[if\s[^\]]+]|\[endif]$/;
|
|
19
|
+
const RE_EVENT_ATTR_DEFAULT = /^on[a-z]{3,}$/;
|
|
20
|
+
const RE_CAN_REMOVE_ATTR_QUOTES = /^[^ \t\n\f\r"'`=<>]+$/;
|
|
21
|
+
const RE_TRAILING_SEMICOLON = /;$/;
|
|
22
|
+
const RE_AMP_ENTITY = /&(#?[0-9a-zA-Z]+;)/g;
|
|
23
|
+
|
|
24
|
+
// Tiny stable stringify for options signatures (sorted keys, shallow, nested objects)
|
|
25
|
+
function stableStringify(obj) {
|
|
26
|
+
if (obj == null || typeof obj !== 'object') return JSON.stringify(obj);
|
|
27
|
+
if (Array.isArray(obj)) return '[' + obj.map(stableStringify).join(',') + ']';
|
|
28
|
+
const keys = Object.keys(obj).sort();
|
|
29
|
+
let out = '{';
|
|
30
|
+
for (let i = 0; i < keys.length; i++) {
|
|
31
|
+
const k = keys[i];
|
|
32
|
+
out += JSON.stringify(k) + ':' + stableStringify(obj[k]) + (i < keys.length - 1 ? ',' : '');
|
|
33
|
+
}
|
|
34
|
+
return out + '}';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Minimal LRU cache for strings and promises
|
|
38
|
+
class LRU {
|
|
39
|
+
constructor(limit = 200) {
|
|
40
|
+
this.limit = limit;
|
|
41
|
+
this.map = new Map();
|
|
42
|
+
}
|
|
43
|
+
get(key) {
|
|
44
|
+
const v = this.map.get(key);
|
|
45
|
+
if (v !== undefined) {
|
|
46
|
+
this.map.delete(key);
|
|
47
|
+
this.map.set(key, v);
|
|
48
|
+
}
|
|
49
|
+
return v;
|
|
50
|
+
}
|
|
51
|
+
set(key, value) {
|
|
52
|
+
if (this.map.has(key)) this.map.delete(key);
|
|
53
|
+
this.map.set(key, value);
|
|
54
|
+
if (this.map.size > this.limit) {
|
|
55
|
+
const first = this.map.keys().next().value;
|
|
56
|
+
this.map.delete(first);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
delete(key) { this.map.delete(key); }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Per-process caches
|
|
63
|
+
const jsMinifyCache = new LRU(200);
|
|
64
|
+
const cssMinifyCache = new LRU(200);
|
|
65
|
+
|
|
66
|
+
const trimWhitespace = str => {
|
|
67
|
+
if (!str) return str;
|
|
68
|
+
// Fast path: if no whitespace at start or end, return early
|
|
69
|
+
if (!/^[ \n\r\t\f]/.test(str) && !/[ \n\r\t\f]$/.test(str)) {
|
|
70
|
+
return str;
|
|
71
|
+
}
|
|
72
|
+
return str.replace(RE_WS_START, '').replace(RE_WS_END, '');
|
|
73
|
+
};
|
|
11
74
|
|
|
12
75
|
function collapseWhitespaceAll(str) {
|
|
76
|
+
if (!str) return str;
|
|
77
|
+
// Fast path: if there are no common whitespace characters, return early
|
|
78
|
+
if (!/[ \n\r\t\f\xA0]/.test(str)) {
|
|
79
|
+
return str;
|
|
80
|
+
}
|
|
13
81
|
// Non-breaking space is specifically handled inside the replacer function here:
|
|
14
|
-
return str
|
|
15
|
-
return spaces === '\t' ? '\t' : spaces.replace(
|
|
82
|
+
return str.replace(RE_ALL_WS_NBSP, function (spaces) {
|
|
83
|
+
return spaces === '\t' ? '\t' : spaces.replace(RE_NBSP_LEADING_GROUP, '$1 ');
|
|
16
84
|
});
|
|
17
85
|
}
|
|
18
86
|
|
|
19
87
|
function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
20
88
|
let lineBreakBefore = ''; let lineBreakAfter = '';
|
|
21
89
|
|
|
90
|
+
if (!str) return str;
|
|
91
|
+
|
|
22
92
|
if (options.preserveLineBreaks) {
|
|
23
93
|
str = str.replace(/^[ \n\r\t\f]*?[\n\r][ \n\r\t\f]*/, function () {
|
|
24
94
|
lineBreakBefore = '\n';
|
|
@@ -36,7 +106,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
36
106
|
if (conservative && spaces === '\t') {
|
|
37
107
|
return '\t';
|
|
38
108
|
}
|
|
39
|
-
return spaces.replace(/^[^\xA0]+/, '').replace(
|
|
109
|
+
return spaces.replace(/^[^\xA0]+/, '').replace(RE_NBSP_LEAD_GROUP, '$1 ') || (conservative ? ' ' : '');
|
|
40
110
|
});
|
|
41
111
|
}
|
|
42
112
|
|
|
@@ -47,7 +117,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
47
117
|
if (conservative && spaces === '\t') {
|
|
48
118
|
return '\t';
|
|
49
119
|
}
|
|
50
|
-
return spaces.replace(
|
|
120
|
+
return spaces.replace(RE_NBSP_TRAILING_GROUP, ' $1').replace(RE_NBSP_TRAILING_STRIP, '') || (conservative ? ' ' : '');
|
|
51
121
|
});
|
|
52
122
|
}
|
|
53
123
|
|
|
@@ -79,7 +149,7 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements,
|
|
|
79
149
|
}
|
|
80
150
|
|
|
81
151
|
function isConditionalComment(text) {
|
|
82
|
-
return
|
|
152
|
+
return RE_CONDITIONAL_COMMENT.test(text);
|
|
83
153
|
}
|
|
84
154
|
|
|
85
155
|
function isIgnoredComment(text, options) {
|
|
@@ -101,12 +171,12 @@ function isEventAttribute(attrName, options) {
|
|
|
101
171
|
}
|
|
102
172
|
return false;
|
|
103
173
|
}
|
|
104
|
-
return
|
|
174
|
+
return RE_EVENT_ATTR_DEFAULT.test(attrName);
|
|
105
175
|
}
|
|
106
176
|
|
|
107
177
|
function canRemoveAttributeQuotes(value) {
|
|
108
178
|
// https://mathiasbynens.be/notes/unquoted-attribute-values
|
|
109
|
-
return
|
|
179
|
+
return RE_CAN_REMOVE_ATTR_QUOTES.test(value);
|
|
110
180
|
}
|
|
111
181
|
|
|
112
182
|
function attributesInclude(attributes, attribute) {
|
|
@@ -317,7 +387,7 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
317
387
|
} else if (attrName === 'style') {
|
|
318
388
|
attrValue = trimWhitespace(attrValue);
|
|
319
389
|
if (attrValue) {
|
|
320
|
-
if (
|
|
390
|
+
if (attrValue.endsWith(';') && !/&#?[0-9a-zA-Z]+;$/.test(attrValue)) {
|
|
321
391
|
attrValue = attrValue.replace(/\s*;$/, ';');
|
|
322
392
|
}
|
|
323
393
|
attrValue = await options.minifyCSS(attrValue, 'inline');
|
|
@@ -636,7 +706,10 @@ async function normalizeAttr(attr, attrs, tag, options) {
|
|
|
636
706
|
let attrValue = attr.value;
|
|
637
707
|
|
|
638
708
|
if (options.decodeEntities && attrValue) {
|
|
639
|
-
|
|
709
|
+
// Fast path: only decode when entities are present
|
|
710
|
+
if (attrValue.indexOf('&') !== -1) {
|
|
711
|
+
attrValue = decodeHTMLStrict(attrValue);
|
|
712
|
+
}
|
|
640
713
|
}
|
|
641
714
|
|
|
642
715
|
if ((options.removeRedundantAttributes &&
|
|
@@ -657,8 +730,8 @@ async function normalizeAttr(attr, attrs, tag, options) {
|
|
|
657
730
|
return;
|
|
658
731
|
}
|
|
659
732
|
|
|
660
|
-
if (options.decodeEntities && attrValue) {
|
|
661
|
-
attrValue = attrValue.replace(
|
|
733
|
+
if (options.decodeEntities && attrValue && attrValue.indexOf('&') !== -1) {
|
|
734
|
+
attrValue = attrValue.replace(RE_AMP_ENTITY, '&$1');
|
|
662
735
|
}
|
|
663
736
|
|
|
664
737
|
return {
|
|
@@ -778,6 +851,10 @@ const processOptions = (inputOptions) => {
|
|
|
778
851
|
const lightningCssOptions = typeof option === 'object' ? option : {};
|
|
779
852
|
|
|
780
853
|
options.minifyCSS = async function (text, type) {
|
|
854
|
+
// Fast path: nothing to minify
|
|
855
|
+
if (!text || !text.trim()) {
|
|
856
|
+
return text;
|
|
857
|
+
}
|
|
781
858
|
text = await replaceAsync(
|
|
782
859
|
text,
|
|
783
860
|
/(url\s*\(\s*)(?:"([^"]*)"|'([^']*)'|([^\s)]+))(\s*\))/ig,
|
|
@@ -796,10 +873,20 @@ const processOptions = (inputOptions) => {
|
|
|
796
873
|
}
|
|
797
874
|
}
|
|
798
875
|
);
|
|
799
|
-
|
|
876
|
+
// Cache key: wrapped content, type, options signature
|
|
800
877
|
const inputCSS = wrapCSS(text, type);
|
|
878
|
+
const cssSig = stableStringify({ type, opts: lightningCssOptions, cont: !!options.continueOnMinifyError });
|
|
879
|
+
// For large inputs, use length and content fingerprint (first/last 50 chars) to prevent collisions
|
|
880
|
+
const cssKey = inputCSS.length > 2048
|
|
881
|
+
? (inputCSS.length + '|' + inputCSS.slice(0, 50) + inputCSS.slice(-50) + '|' + type + '|' + cssSig)
|
|
882
|
+
: (inputCSS + '|' + type + '|' + cssSig);
|
|
801
883
|
|
|
802
884
|
try {
|
|
885
|
+
const cached = cssMinifyCache.get(cssKey);
|
|
886
|
+
if (cached) {
|
|
887
|
+
return cached;
|
|
888
|
+
}
|
|
889
|
+
|
|
803
890
|
const result = transformCSS({
|
|
804
891
|
filename: 'input.css',
|
|
805
892
|
code: Buffer.from(inputCSS),
|
|
@@ -822,12 +909,12 @@ const processOptions = (inputOptions) => {
|
|
|
822
909
|
|
|
823
910
|
// Preserve if output is empty and input had template syntax or UIDs
|
|
824
911
|
// This catches cases where Lightning CSS removed content that should be preserved
|
|
825
|
-
|
|
826
|
-
return text;
|
|
827
|
-
}
|
|
912
|
+
const finalOutput = (text.trim() && !outputCSS.trim() && (looksLikeTemplate || hasUID)) ? text : outputCSS;
|
|
828
913
|
|
|
829
|
-
|
|
914
|
+
cssMinifyCache.set(cssKey, finalOutput);
|
|
915
|
+
return finalOutput;
|
|
830
916
|
} catch (err) {
|
|
917
|
+
cssMinifyCache.delete(cssKey);
|
|
831
918
|
if (!options.continueOnMinifyError) {
|
|
832
919
|
throw err;
|
|
833
920
|
}
|
|
@@ -853,10 +940,39 @@ const processOptions = (inputOptions) => {
|
|
|
853
940
|
|
|
854
941
|
terserOptions.parse.bare_returns = inline;
|
|
855
942
|
|
|
943
|
+
let jsKey;
|
|
856
944
|
try {
|
|
857
|
-
|
|
858
|
-
|
|
945
|
+
// Fast path: avoid invoking Terser for empty/whitespace-only content
|
|
946
|
+
if (!code || !code.trim()) {
|
|
947
|
+
return '';
|
|
948
|
+
}
|
|
949
|
+
// Cache key: content, inline, options signature (subset)
|
|
950
|
+
const terserSig = stableStringify({
|
|
951
|
+
compress: terserOptions.compress,
|
|
952
|
+
mangle: terserOptions.mangle,
|
|
953
|
+
ecma: terserOptions.ecma,
|
|
954
|
+
toplevel: terserOptions.toplevel,
|
|
955
|
+
module: terserOptions.module,
|
|
956
|
+
keep_fnames: terserOptions.keep_fnames,
|
|
957
|
+
format: terserOptions.format,
|
|
958
|
+
cont: !!options.continueOnMinifyError,
|
|
959
|
+
});
|
|
960
|
+
// For large inputs, use length and content fingerprint (first/last 50 chars) to prevent collisions
|
|
961
|
+
jsKey = (code.length > 2048 ? (code.length + '|' + code.slice(0, 50) + code.slice(-50) + '|') : (code + '|')) + (inline ? '1' : '0') + '|' + terserSig;
|
|
962
|
+
const cached = jsMinifyCache.get(jsKey);
|
|
963
|
+
if (cached) {
|
|
964
|
+
return await cached;
|
|
965
|
+
}
|
|
966
|
+
const inFlight = (async () => {
|
|
967
|
+
const result = await terser(code, terserOptions);
|
|
968
|
+
return result.code.replace(RE_TRAILING_SEMICOLON, '');
|
|
969
|
+
})();
|
|
970
|
+
jsMinifyCache.set(jsKey, inFlight);
|
|
971
|
+
const resolved = await inFlight;
|
|
972
|
+
jsMinifyCache.set(jsKey, resolved);
|
|
973
|
+
return resolved;
|
|
859
974
|
} catch (err) {
|
|
975
|
+
if (jsKey) jsMinifyCache.delete(jsKey);
|
|
860
976
|
if (!options.continueOnMinifyError) {
|
|
861
977
|
throw err;
|
|
862
978
|
}
|
|
@@ -1318,7 +1434,9 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1318
1434
|
prevTag = prevTag === '' ? 'comment' : prevTag;
|
|
1319
1435
|
nextTag = nextTag === '' ? 'comment' : nextTag;
|
|
1320
1436
|
if (options.decodeEntities && text && !specialContentTags.has(currentTag)) {
|
|
1321
|
-
|
|
1437
|
+
if (text.indexOf('&') !== -1) {
|
|
1438
|
+
text = decodeHTML(text);
|
|
1439
|
+
}
|
|
1322
1440
|
}
|
|
1323
1441
|
if (options.collapseWhitespace) {
|
|
1324
1442
|
if (!stackNoTrimWhitespace.length) {
|
|
@@ -1392,11 +1510,16 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1392
1510
|
charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
|
|
1393
1511
|
if (options.decodeEntities && text && !specialContentTags.has(currentTag)) {
|
|
1394
1512
|
// Escape any `&` symbols that start either:
|
|
1395
|
-
// 1) a legacy named character reference (i.e
|
|
1396
|
-
// 2) or any other character reference (i.e
|
|
1513
|
+
// 1) a legacy named character reference (i.e., one that doesn’t end with `;`)
|
|
1514
|
+
// 2) or any other character reference (i.e., one that does end with `;`)
|
|
1397
1515
|
// Note that `&` can be escaped as `&`, without the semi-colon.
|
|
1398
1516
|
// https://mathiasbynens.be/notes/ambiguous-ampersands
|
|
1399
|
-
|
|
1517
|
+
if (text.indexOf('&') !== -1) {
|
|
1518
|
+
text = text.replace(/&((?:Iacute|aacute|uacute|plusmn|Otilde|otilde|agrave|Agrave|Yacute|yacute|Oslash|oslash|atilde|Atilde|brvbar|ccedil|Ccedil|Ograve|curren|divide|eacute|Eacute|ograve|Oacute|egrave|Egrave|Ugrave|frac12|frac14|frac34|ugrave|oacute|iacute|Ntilde|ntilde|Uacute|middot|igrave|Igrave|iquest|Aacute|cedil|laquo|micro|iexcl|Icirc|icirc|acirc|Ucirc|Ecirc|ocirc|Ocirc|ecirc|ucirc|Aring|aring|AElig|aelig|acute|pound|raquo|Acirc|times|THORN|szlig|thorn|COPY|auml|ordf|ordm|Uuml|macr|uuml|Auml|ouml|Ouml|para|nbsp|euml|quot|QUOT|Euml|yuml|cent|sect|copy|sup1|sup2|sup3|iuml|Iuml|ETH|shy|reg|not|yen|amp|AMP|REG|uml|eth|deg|gt|GT|LT|lt)(?!;)|(?:#?[0-9a-zA-Z]+;))/g, '&$1');
|
|
1519
|
+
}
|
|
1520
|
+
if (text.indexOf('<') !== -1) {
|
|
1521
|
+
text = text.replace(/</g, '<');
|
|
1522
|
+
}
|
|
1400
1523
|
}
|
|
1401
1524
|
if (uidPattern && options.collapseWhitespace && stackNoTrimWhitespace.length) {
|
|
1402
1525
|
text = text.replace(uidPattern, function (match, prefix, index) {
|