html-minifier-next 5.1.0 → 5.1.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/dist/htmlminifier.cjs +73 -34
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/lib/attributes.d.ts.map +1 -1
- package/dist/types/lib/whitespace.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/htmlminifier.js +22 -9
- package/src/lib/attributes.js +46 -23
- package/src/lib/whitespace.js +5 -2
- package/dist/htmlminifier.esm.bundle.js +0 -41262
package/dist/htmlminifier.cjs
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
-
var entities = require('entities');
|
|
6
|
-
|
|
7
5
|
/*
|
|
8
6
|
* HTML Parser By John Resig (ejohn.org)
|
|
9
7
|
* Modified by Juriy “kangax” Zaytsev
|
|
@@ -1387,12 +1385,15 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, prevAttrs, nextAttrs, op
|
|
|
1387
1385
|
|
|
1388
1386
|
// Collapse/trim whitespace for given tag
|
|
1389
1387
|
|
|
1388
|
+
const noCollapseWsTags = new Set(['script', 'style', 'pre', 'textarea']);
|
|
1389
|
+
const noTrimWsTags = new Set(['pre', 'textarea']);
|
|
1390
|
+
|
|
1390
1391
|
function canCollapseWhitespace(tag) {
|
|
1391
|
-
return
|
|
1392
|
+
return !noCollapseWsTags.has(tag);
|
|
1392
1393
|
}
|
|
1393
1394
|
|
|
1394
1395
|
function canTrimWhitespace(tag) {
|
|
1395
|
-
return
|
|
1396
|
+
return !noTrimWsTags.has(tag);
|
|
1396
1397
|
}
|
|
1397
1398
|
|
|
1398
1399
|
/**
|
|
@@ -2052,6 +2053,16 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
2052
2053
|
// Imports
|
|
2053
2054
|
|
|
2054
2055
|
|
|
2056
|
+
// Lazy-load entities only when `decodeEntities` is enabled
|
|
2057
|
+
|
|
2058
|
+
let decodeHTMLStrictPromise;
|
|
2059
|
+
async function getDecodeHTMLStrict() {
|
|
2060
|
+
if (!decodeHTMLStrictPromise) {
|
|
2061
|
+
decodeHTMLStrictPromise = import('entities').then(m => m.decodeHTMLStrict);
|
|
2062
|
+
}
|
|
2063
|
+
return decodeHTMLStrictPromise;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2055
2066
|
// Validators
|
|
2056
2067
|
|
|
2057
2068
|
function isConditionalComment(text) {
|
|
@@ -2212,31 +2223,45 @@ function isBooleanAttribute(attrName, attrValue) {
|
|
|
2212
2223
|
(attrValue === '' && emptyCollapsible.has(attrName));
|
|
2213
2224
|
}
|
|
2214
2225
|
|
|
2226
|
+
const uriTypeAttributes = new Map([
|
|
2227
|
+
['a', new Set(['href'])],
|
|
2228
|
+
['area', new Set(['href'])],
|
|
2229
|
+
['link', new Set(['href'])],
|
|
2230
|
+
['base', new Set(['href'])],
|
|
2231
|
+
['img', new Set(['src', 'longdesc', 'usemap'])],
|
|
2232
|
+
['object', new Set(['classid', 'codebase', 'data', 'usemap'])],
|
|
2233
|
+
['q', new Set(['cite'])],
|
|
2234
|
+
['blockquote', new Set(['cite'])],
|
|
2235
|
+
['ins', new Set(['cite'])],
|
|
2236
|
+
['del', new Set(['cite'])],
|
|
2237
|
+
['form', new Set(['action'])],
|
|
2238
|
+
['input', new Set(['src', 'usemap'])],
|
|
2239
|
+
['head', new Set(['profile'])],
|
|
2240
|
+
['script', new Set(['src', 'for'])]
|
|
2241
|
+
]);
|
|
2242
|
+
|
|
2215
2243
|
function isUriTypeAttribute(attrName, tag) {
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
(tag === 'img' && /^(?:src|longdesc|usemap)$/.test(attrName)) ||
|
|
2219
|
-
(tag === 'object' && /^(?:classid|codebase|data|usemap)$/.test(attrName)) ||
|
|
2220
|
-
(tag === 'q' && attrName === 'cite') ||
|
|
2221
|
-
(tag === 'blockquote' && attrName === 'cite') ||
|
|
2222
|
-
((tag === 'ins' || tag === 'del') && attrName === 'cite') ||
|
|
2223
|
-
(tag === 'form' && attrName === 'action') ||
|
|
2224
|
-
(tag === 'input' && (attrName === 'src' || attrName === 'usemap')) ||
|
|
2225
|
-
(tag === 'head' && attrName === 'profile') ||
|
|
2226
|
-
(tag === 'script' && (attrName === 'src' || attrName === 'for'))
|
|
2227
|
-
);
|
|
2244
|
+
const set = uriTypeAttributes.get(tag);
|
|
2245
|
+
return set ? set.has(attrName) : false;
|
|
2228
2246
|
}
|
|
2229
2247
|
|
|
2248
|
+
const numberTypeAttributes = new Map([
|
|
2249
|
+
['a', new Set(['tabindex'])],
|
|
2250
|
+
['area', new Set(['tabindex'])],
|
|
2251
|
+
['object', new Set(['tabindex'])],
|
|
2252
|
+
['button', new Set(['tabindex'])],
|
|
2253
|
+
['input', new Set(['maxlength', 'tabindex'])],
|
|
2254
|
+
['select', new Set(['size', 'tabindex'])],
|
|
2255
|
+
['textarea', new Set(['rows', 'cols', 'tabindex'])],
|
|
2256
|
+
['colgroup', new Set(['span'])],
|
|
2257
|
+
['col', new Set(['span'])],
|
|
2258
|
+
['th', new Set(['rowspan', 'colspan'])],
|
|
2259
|
+
['td', new Set(['rowspan', 'colspan'])]
|
|
2260
|
+
]);
|
|
2261
|
+
|
|
2230
2262
|
function isNumberTypeAttribute(attrName, tag) {
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
(tag === 'input' && (attrName === 'maxlength' || attrName === 'tabindex')) ||
|
|
2234
|
-
(tag === 'select' && (attrName === 'size' || attrName === 'tabindex')) ||
|
|
2235
|
-
(tag === 'textarea' && /^(?:rows|cols|tabindex)$/.test(attrName)) ||
|
|
2236
|
-
(tag === 'colgroup' && attrName === 'span') ||
|
|
2237
|
-
(tag === 'col' && attrName === 'span') ||
|
|
2238
|
-
((tag === 'th' || tag === 'td') && (attrName === 'rowspan' || attrName === 'colspan'))
|
|
2239
|
-
);
|
|
2263
|
+
const set = numberTypeAttributes.get(tag);
|
|
2264
|
+
return set ? set.has(attrName) : false;
|
|
2240
2265
|
}
|
|
2241
2266
|
|
|
2242
2267
|
function isLinkType(tag, attrs, value) {
|
|
@@ -2467,7 +2492,7 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
2467
2492
|
if (options.decodeEntities && attrValue) {
|
|
2468
2493
|
// Fast path: Only decode when entities are present
|
|
2469
2494
|
if (attrValue.indexOf('&') !== -1) {
|
|
2470
|
-
attrValue =
|
|
2495
|
+
attrValue = (await getDecodeHTMLStrict())(attrValue);
|
|
2471
2496
|
}
|
|
2472
2497
|
}
|
|
2473
2498
|
|
|
@@ -2891,6 +2916,14 @@ async function getSvgo() {
|
|
|
2891
2916
|
return svgoPromise;
|
|
2892
2917
|
}
|
|
2893
2918
|
|
|
2919
|
+
let decodeHTMLPromise;
|
|
2920
|
+
async function getDecodeHTML() {
|
|
2921
|
+
if (!decodeHTMLPromise) {
|
|
2922
|
+
decodeHTMLPromise = import('entities').then(m => m.decodeHTML);
|
|
2923
|
+
}
|
|
2924
|
+
return decodeHTMLPromise;
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2894
2927
|
// Minification caches (initialized on first use with configurable sizes)
|
|
2895
2928
|
let cssMinifyCache = null;
|
|
2896
2929
|
let jsMinifyCache = null;
|
|
@@ -3675,6 +3708,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3675
3708
|
let currentAttrs = [];
|
|
3676
3709
|
const stackNoTrimWhitespace = [];
|
|
3677
3710
|
const stackNoCollapseWhitespace = [];
|
|
3711
|
+
let preTextareaDepth = 0; // Count of `pre`/`textarea` entries in `stackNoTrimWhitespace`
|
|
3678
3712
|
let optionalStartTag = '';
|
|
3679
3713
|
let optionalEndTag = '';
|
|
3680
3714
|
let optionalEndTagEmitted = false;
|
|
@@ -3835,7 +3869,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3835
3869
|
let charsIndex = buffer.length - 1;
|
|
3836
3870
|
if (buffer.length > 1) {
|
|
3837
3871
|
const item = buffer[buffer.length - 1];
|
|
3838
|
-
if (/^(?:<!|$)/.test(item) && item.indexOf(uidIgnore) === -1) {
|
|
3872
|
+
if (/^(?:<!|$)/.test(item) && (!uidIgnore || item.indexOf(uidIgnore) === -1)) {
|
|
3839
3873
|
charsIndex--;
|
|
3840
3874
|
}
|
|
3841
3875
|
}
|
|
@@ -3929,6 +3963,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3929
3963
|
if (!unary) {
|
|
3930
3964
|
if (!canTrimWhitespace$1(tag, attrs) || stackNoTrimWhitespace.length) {
|
|
3931
3965
|
stackNoTrimWhitespace.push(tag);
|
|
3966
|
+
if (tag === 'pre' || tag === 'textarea') preTextareaDepth++;
|
|
3932
3967
|
}
|
|
3933
3968
|
if (!canCollapseWhitespace$1(tag, attrs) || stackNoCollapseWhitespace.length) {
|
|
3934
3969
|
stackNoCollapseWhitespace.push(tag);
|
|
@@ -3958,11 +3993,14 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3958
3993
|
options.sortAttributes(tag, attrs);
|
|
3959
3994
|
}
|
|
3960
3995
|
|
|
3996
|
+
const normalizedAttrs = await Promise.all(
|
|
3997
|
+
attrs.map(attr => normalizeAttr(attr, attrs, tag, options, minifyHTML))
|
|
3998
|
+
);
|
|
3961
3999
|
const parts = [];
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
if (
|
|
3965
|
-
parts.push(buildAttr(
|
|
4000
|
+
let isLast = true;
|
|
4001
|
+
for (let i = normalizedAttrs.length - 1; i >= 0; i--) {
|
|
4002
|
+
if (normalizedAttrs[i]) {
|
|
4003
|
+
parts.push(buildAttr(normalizedAttrs[i], hasUnarySlash, options, isLast, uidAttr));
|
|
3966
4004
|
isLast = false;
|
|
3967
4005
|
}
|
|
3968
4006
|
}
|
|
@@ -3998,6 +4036,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3998
4036
|
if (options.collapseWhitespace) {
|
|
3999
4037
|
if (stackNoTrimWhitespace.length) {
|
|
4000
4038
|
if (tag === stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1]) {
|
|
4039
|
+
if (tag === 'pre' || tag === 'textarea') preTextareaDepth--;
|
|
4001
4040
|
stackNoTrimWhitespace.pop();
|
|
4002
4041
|
}
|
|
4003
4042
|
} else {
|
|
@@ -4091,7 +4130,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4091
4130
|
nextAttrs = nextAttrs || [];
|
|
4092
4131
|
if (options.decodeEntities && text && !specialContentElements.has(currentTag)) {
|
|
4093
4132
|
if (text.indexOf('&') !== -1) {
|
|
4094
|
-
text =
|
|
4133
|
+
text = (await getDecodeHTML())(text);
|
|
4095
4134
|
}
|
|
4096
4135
|
}
|
|
4097
4136
|
// Trim outermost newline-based whitespace inside `pre`/`textarea` elements
|
|
@@ -4099,7 +4138,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4099
4138
|
// Only trims single trailing newlines (multiple newlines are likely intentional formatting)
|
|
4100
4139
|
if (options.collapseWhitespace && stackNoTrimWhitespace.length) {
|
|
4101
4140
|
const topTag = stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1];
|
|
4102
|
-
if (
|
|
4141
|
+
if (preTextareaDepth > 0) {
|
|
4103
4142
|
// Trim trailing whitespace only if it ends with a single newline (not multiple)
|
|
4104
4143
|
// Multiple newlines are likely intentional formatting, single newline is often a template artifact
|
|
4105
4144
|
// Treat CRLF (`\r\n`), CR (`\r`), and LF (`\n`) as single line-ending units
|
|
@@ -4112,7 +4151,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4112
4151
|
if (!stackNoTrimWhitespace.length) {
|
|
4113
4152
|
if (prevTag === 'comment') {
|
|
4114
4153
|
const prevComment = buffer[buffer.length - 1];
|
|
4115
|
-
if (prevComment.indexOf(uidIgnore) === -1) {
|
|
4154
|
+
if (!uidIgnore || prevComment.indexOf(uidIgnore) === -1) {
|
|
4116
4155
|
if (!prevComment) {
|
|
4117
4156
|
prevTag = charsPrevTag;
|
|
4118
4157
|
}
|
|
@@ -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":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attributes.d.ts","sourceRoot":"","sources":["../../../src/lib/attributes.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"attributes.d.ts","sourceRoot":"","sources":["../../../src/lib/attributes.js"],"names":[],"mappings":"AAmCA,yDAEC;AAED,mEAOC;AAED,uEAWC;AAED,8DAGC;AAED,4EAOC;AAgCD,mGAuCC;AAED,mEAGC;AAED,qEAGC;AAED,kEAWC;AAED,sEAGC;AAED,8DAWC;AAED,2EAIC;AAmBD,qEAGC;AAgBD,wEAGC;AAED,sEAUC;AAED,2EAEC;AAED,2DAEC;AAED,8DAUC;AAED,uEAUC;AAED,oGASC;AAED,4DAOC;AAID,0IAqIC;AAsBD;;;;GAsCC;AAED,6GAuHC;AA3hBD;;;;;;;GAOG;AACH,mEAHW,OAAO,SAuBjB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"whitespace.d.ts","sourceRoot":"","sources":["../../../src/lib/whitespace.js"],"names":[],"mappings":"AAiBA,8CAOC;AAID,qDAgBC;AAID,iHAqFC;AAID,0KAwEC;
|
|
1
|
+
{"version":3,"file":"whitespace.d.ts","sourceRoot":"","sources":["../../../src/lib/whitespace.js"],"names":[],"mappings":"AAiBA,8CAOC;AAID,qDAgBC;AAID,iHAqFC;AAID,0KAwEC;AAOD,yDAEC;AAED,qDAEC"}
|
package/package.json
CHANGED
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
"description": "Super-configurable and well-tested web page minifier (enhanced successor of HTML Minifier)",
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"@commitlint/cli": "^20.3.1",
|
|
17
|
-
"@eslint/js": "^
|
|
17
|
+
"@eslint/js": "^10.0.1",
|
|
18
18
|
"@rollup/plugin-commonjs": "^29.0.0",
|
|
19
19
|
"@rollup/plugin-json": "^6.1.0",
|
|
20
20
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
21
21
|
"@swc/core": "^1.15.11",
|
|
22
|
-
"eslint": "^
|
|
22
|
+
"eslint": "^10.0.0",
|
|
23
23
|
"rollup": "^4.57.1",
|
|
24
24
|
"rollup-plugin-polyfill-node": "^0.13.0",
|
|
25
25
|
"typescript": "^5.9.3",
|
|
@@ -31,12 +31,12 @@
|
|
|
31
31
|
"import": "./src/htmlminifier.js",
|
|
32
32
|
"require": "./dist/htmlminifier.cjs"
|
|
33
33
|
},
|
|
34
|
-
"./dist/*": "./dist/*.js",
|
|
35
34
|
"./package.json": "./package.json"
|
|
36
35
|
},
|
|
37
36
|
"files": [
|
|
38
37
|
"cli.js",
|
|
39
|
-
"dist/",
|
|
38
|
+
"dist/htmlminifier.cjs",
|
|
39
|
+
"dist/types/",
|
|
40
40
|
"src/"
|
|
41
41
|
],
|
|
42
42
|
"funding": "https://github.com/j9t/html-minifier-next?sponsor=1",
|
|
@@ -95,5 +95,5 @@
|
|
|
95
95
|
},
|
|
96
96
|
"type": "module",
|
|
97
97
|
"types": "./dist/types/htmlminifier.d.ts",
|
|
98
|
-
"version": "5.1.
|
|
99
|
-
}
|
|
98
|
+
"version": "5.1.2"
|
|
99
|
+
}
|
package/src/htmlminifier.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// Imports
|
|
2
2
|
|
|
3
|
-
import { decodeHTML } from 'entities';
|
|
4
3
|
import { HTMLParser, endTag } from './htmlparser.js';
|
|
5
4
|
import TokenChain from './tokenchain.js';
|
|
6
5
|
import { presets, getPreset, getPresetNames } from './presets.js';
|
|
@@ -100,6 +99,14 @@ async function getSvgo() {
|
|
|
100
99
|
return svgoPromise;
|
|
101
100
|
}
|
|
102
101
|
|
|
102
|
+
let decodeHTMLPromise;
|
|
103
|
+
async function getDecodeHTML() {
|
|
104
|
+
if (!decodeHTMLPromise) {
|
|
105
|
+
decodeHTMLPromise = import('entities').then(m => m.decodeHTML);
|
|
106
|
+
}
|
|
107
|
+
return decodeHTMLPromise;
|
|
108
|
+
}
|
|
109
|
+
|
|
103
110
|
// Minification caches (initialized on first use with configurable sizes)
|
|
104
111
|
let cssMinifyCache = null;
|
|
105
112
|
let jsMinifyCache = null;
|
|
@@ -888,6 +895,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
888
895
|
let currentAttrs = [];
|
|
889
896
|
const stackNoTrimWhitespace = [];
|
|
890
897
|
const stackNoCollapseWhitespace = [];
|
|
898
|
+
let preTextareaDepth = 0; // Count of `pre`/`textarea` entries in `stackNoTrimWhitespace`
|
|
891
899
|
let optionalStartTag = '';
|
|
892
900
|
let optionalEndTag = '';
|
|
893
901
|
let optionalEndTagEmitted = false;
|
|
@@ -1048,7 +1056,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1048
1056
|
let charsIndex = buffer.length - 1;
|
|
1049
1057
|
if (buffer.length > 1) {
|
|
1050
1058
|
const item = buffer[buffer.length - 1];
|
|
1051
|
-
if (/^(?:<!|$)/.test(item) && item.indexOf(uidIgnore) === -1) {
|
|
1059
|
+
if (/^(?:<!|$)/.test(item) && (!uidIgnore || item.indexOf(uidIgnore) === -1)) {
|
|
1052
1060
|
charsIndex--;
|
|
1053
1061
|
}
|
|
1054
1062
|
}
|
|
@@ -1142,6 +1150,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1142
1150
|
if (!unary) {
|
|
1143
1151
|
if (!canTrimWhitespace(tag, attrs) || stackNoTrimWhitespace.length) {
|
|
1144
1152
|
stackNoTrimWhitespace.push(tag);
|
|
1153
|
+
if (tag === 'pre' || tag === 'textarea') preTextareaDepth++;
|
|
1145
1154
|
}
|
|
1146
1155
|
if (!canCollapseWhitespace(tag, attrs) || stackNoCollapseWhitespace.length) {
|
|
1147
1156
|
stackNoCollapseWhitespace.push(tag);
|
|
@@ -1171,11 +1180,14 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1171
1180
|
options.sortAttributes(tag, attrs);
|
|
1172
1181
|
}
|
|
1173
1182
|
|
|
1183
|
+
const normalizedAttrs = await Promise.all(
|
|
1184
|
+
attrs.map(attr => normalizeAttr(attr, attrs, tag, options, minifyHTML))
|
|
1185
|
+
);
|
|
1174
1186
|
const parts = [];
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
if (
|
|
1178
|
-
parts.push(buildAttr(
|
|
1187
|
+
let isLast = true;
|
|
1188
|
+
for (let i = normalizedAttrs.length - 1; i >= 0; i--) {
|
|
1189
|
+
if (normalizedAttrs[i]) {
|
|
1190
|
+
parts.push(buildAttr(normalizedAttrs[i], hasUnarySlash, options, isLast, uidAttr));
|
|
1179
1191
|
isLast = false;
|
|
1180
1192
|
}
|
|
1181
1193
|
}
|
|
@@ -1211,6 +1223,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1211
1223
|
if (options.collapseWhitespace) {
|
|
1212
1224
|
if (stackNoTrimWhitespace.length) {
|
|
1213
1225
|
if (tag === stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1]) {
|
|
1226
|
+
if (tag === 'pre' || tag === 'textarea') preTextareaDepth--;
|
|
1214
1227
|
stackNoTrimWhitespace.pop();
|
|
1215
1228
|
}
|
|
1216
1229
|
} else {
|
|
@@ -1304,7 +1317,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1304
1317
|
nextAttrs = nextAttrs || [];
|
|
1305
1318
|
if (options.decodeEntities && text && !specialContentElements.has(currentTag)) {
|
|
1306
1319
|
if (text.indexOf('&') !== -1) {
|
|
1307
|
-
text =
|
|
1320
|
+
text = (await getDecodeHTML())(text);
|
|
1308
1321
|
}
|
|
1309
1322
|
}
|
|
1310
1323
|
// Trim outermost newline-based whitespace inside `pre`/`textarea` elements
|
|
@@ -1312,7 +1325,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1312
1325
|
// Only trims single trailing newlines (multiple newlines are likely intentional formatting)
|
|
1313
1326
|
if (options.collapseWhitespace && stackNoTrimWhitespace.length) {
|
|
1314
1327
|
const topTag = stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1];
|
|
1315
|
-
if (
|
|
1328
|
+
if (preTextareaDepth > 0) {
|
|
1316
1329
|
// Trim trailing whitespace only if it ends with a single newline (not multiple)
|
|
1317
1330
|
// Multiple newlines are likely intentional formatting, single newline is often a template artifact
|
|
1318
1331
|
// Treat CRLF (`\r\n`), CR (`\r`), and LF (`\n`) as single line-ending units
|
|
@@ -1325,7 +1338,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1325
1338
|
if (!stackNoTrimWhitespace.length) {
|
|
1326
1339
|
if (prevTag === 'comment') {
|
|
1327
1340
|
const prevComment = buffer[buffer.length - 1];
|
|
1328
|
-
if (prevComment.indexOf(uidIgnore) === -1) {
|
|
1341
|
+
if (!uidIgnore || prevComment.indexOf(uidIgnore) === -1) {
|
|
1329
1342
|
if (!prevComment) {
|
|
1330
1343
|
prevTag = charsPrevTag;
|
|
1331
1344
|
}
|
package/src/lib/attributes.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// Imports
|
|
2
2
|
|
|
3
|
-
import { decodeHTMLStrict } from 'entities';
|
|
4
3
|
import {
|
|
5
4
|
RE_CONDITIONAL_COMMENT,
|
|
6
5
|
RE_EVENT_ATTR_DEFAULT,
|
|
@@ -22,6 +21,16 @@ import {
|
|
|
22
21
|
import { trimWhitespace, collapseWhitespaceAll } from './whitespace.js';
|
|
23
22
|
import { shouldMinifyInnerHTML } from './options.js';
|
|
24
23
|
|
|
24
|
+
// Lazy-load entities only when `decodeEntities` is enabled
|
|
25
|
+
|
|
26
|
+
let decodeHTMLStrictPromise;
|
|
27
|
+
async function getDecodeHTMLStrict() {
|
|
28
|
+
if (!decodeHTMLStrictPromise) {
|
|
29
|
+
decodeHTMLStrictPromise = import('entities').then(m => m.decodeHTMLStrict);
|
|
30
|
+
}
|
|
31
|
+
return decodeHTMLStrictPromise;
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
// Validators
|
|
26
35
|
|
|
27
36
|
function isConditionalComment(text) {
|
|
@@ -182,31 +191,45 @@ function isBooleanAttribute(attrName, attrValue) {
|
|
|
182
191
|
(attrValue === '' && emptyCollapsible.has(attrName));
|
|
183
192
|
}
|
|
184
193
|
|
|
194
|
+
const uriTypeAttributes = new Map([
|
|
195
|
+
['a', new Set(['href'])],
|
|
196
|
+
['area', new Set(['href'])],
|
|
197
|
+
['link', new Set(['href'])],
|
|
198
|
+
['base', new Set(['href'])],
|
|
199
|
+
['img', new Set(['src', 'longdesc', 'usemap'])],
|
|
200
|
+
['object', new Set(['classid', 'codebase', 'data', 'usemap'])],
|
|
201
|
+
['q', new Set(['cite'])],
|
|
202
|
+
['blockquote', new Set(['cite'])],
|
|
203
|
+
['ins', new Set(['cite'])],
|
|
204
|
+
['del', new Set(['cite'])],
|
|
205
|
+
['form', new Set(['action'])],
|
|
206
|
+
['input', new Set(['src', 'usemap'])],
|
|
207
|
+
['head', new Set(['profile'])],
|
|
208
|
+
['script', new Set(['src', 'for'])]
|
|
209
|
+
]);
|
|
210
|
+
|
|
185
211
|
function isUriTypeAttribute(attrName, tag) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
(tag === 'img' && /^(?:src|longdesc|usemap)$/.test(attrName)) ||
|
|
189
|
-
(tag === 'object' && /^(?:classid|codebase|data|usemap)$/.test(attrName)) ||
|
|
190
|
-
(tag === 'q' && attrName === 'cite') ||
|
|
191
|
-
(tag === 'blockquote' && attrName === 'cite') ||
|
|
192
|
-
((tag === 'ins' || tag === 'del') && attrName === 'cite') ||
|
|
193
|
-
(tag === 'form' && attrName === 'action') ||
|
|
194
|
-
(tag === 'input' && (attrName === 'src' || attrName === 'usemap')) ||
|
|
195
|
-
(tag === 'head' && attrName === 'profile') ||
|
|
196
|
-
(tag === 'script' && (attrName === 'src' || attrName === 'for'))
|
|
197
|
-
);
|
|
212
|
+
const set = uriTypeAttributes.get(tag);
|
|
213
|
+
return set ? set.has(attrName) : false;
|
|
198
214
|
}
|
|
199
215
|
|
|
216
|
+
const numberTypeAttributes = new Map([
|
|
217
|
+
['a', new Set(['tabindex'])],
|
|
218
|
+
['area', new Set(['tabindex'])],
|
|
219
|
+
['object', new Set(['tabindex'])],
|
|
220
|
+
['button', new Set(['tabindex'])],
|
|
221
|
+
['input', new Set(['maxlength', 'tabindex'])],
|
|
222
|
+
['select', new Set(['size', 'tabindex'])],
|
|
223
|
+
['textarea', new Set(['rows', 'cols', 'tabindex'])],
|
|
224
|
+
['colgroup', new Set(['span'])],
|
|
225
|
+
['col', new Set(['span'])],
|
|
226
|
+
['th', new Set(['rowspan', 'colspan'])],
|
|
227
|
+
['td', new Set(['rowspan', 'colspan'])]
|
|
228
|
+
]);
|
|
229
|
+
|
|
200
230
|
function isNumberTypeAttribute(attrName, tag) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
(tag === 'input' && (attrName === 'maxlength' || attrName === 'tabindex')) ||
|
|
204
|
-
(tag === 'select' && (attrName === 'size' || attrName === 'tabindex')) ||
|
|
205
|
-
(tag === 'textarea' && /^(?:rows|cols|tabindex)$/.test(attrName)) ||
|
|
206
|
-
(tag === 'colgroup' && attrName === 'span') ||
|
|
207
|
-
(tag === 'col' && attrName === 'span') ||
|
|
208
|
-
((tag === 'th' || tag === 'td') && (attrName === 'rowspan' || attrName === 'colspan'))
|
|
209
|
-
);
|
|
231
|
+
const set = numberTypeAttributes.get(tag);
|
|
232
|
+
return set ? set.has(attrName) : false;
|
|
210
233
|
}
|
|
211
234
|
|
|
212
235
|
function isLinkType(tag, attrs, value) {
|
|
@@ -437,7 +460,7 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
437
460
|
if (options.decodeEntities && attrValue) {
|
|
438
461
|
// Fast path: Only decode when entities are present
|
|
439
462
|
if (attrValue.indexOf('&') !== -1) {
|
|
440
|
-
attrValue =
|
|
463
|
+
attrValue = (await getDecodeHTMLStrict())(attrValue);
|
|
441
464
|
}
|
|
442
465
|
}
|
|
443
466
|
|
package/src/lib/whitespace.js
CHANGED
|
@@ -211,12 +211,15 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, prevAttrs, nextAttrs, op
|
|
|
211
211
|
|
|
212
212
|
// Collapse/trim whitespace for given tag
|
|
213
213
|
|
|
214
|
+
const noCollapseWsTags = new Set(['script', 'style', 'pre', 'textarea']);
|
|
215
|
+
const noTrimWsTags = new Set(['pre', 'textarea']);
|
|
216
|
+
|
|
214
217
|
function canCollapseWhitespace(tag) {
|
|
215
|
-
return
|
|
218
|
+
return !noCollapseWsTags.has(tag);
|
|
216
219
|
}
|
|
217
220
|
|
|
218
221
|
function canTrimWhitespace(tag) {
|
|
219
|
-
return
|
|
222
|
+
return !noTrimWsTags.has(tag);
|
|
220
223
|
}
|
|
221
224
|
|
|
222
225
|
// Exports
|