html-minifier-next 4.12.2 → 4.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -26
- package/cli.js +1 -1
- package/dist/htmlminifier.cjs +1552 -1320
- package/dist/htmlminifier.esm.bundle.js +4204 -3972
- package/dist/types/htmlminifier.d.ts +10 -3
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/htmlparser.d.ts.map +1 -1
- package/dist/types/lib/attributes.d.ts +29 -0
- package/dist/types/lib/attributes.d.ts.map +1 -0
- package/dist/types/lib/constants.d.ts +83 -0
- package/dist/types/lib/constants.d.ts.map +1 -0
- package/dist/types/lib/content.d.ts +7 -0
- package/dist/types/lib/content.d.ts.map +1 -0
- package/dist/types/lib/elements.d.ts +39 -0
- package/dist/types/lib/elements.d.ts.map +1 -0
- package/dist/types/lib/options.d.ts +17 -0
- package/dist/types/lib/options.d.ts.map +1 -0
- package/dist/types/lib/utils.d.ts +21 -0
- package/dist/types/lib/utils.d.ts.map +1 -0
- package/dist/types/lib/whitespace.d.ts +7 -0
- package/dist/types/lib/whitespace.d.ts.map +1 -0
- package/dist/types/presets.d.ts.map +1 -1
- package/package.json +10 -1
- package/src/htmlminifier.js +114 -1229
- package/src/htmlparser.js +11 -11
- package/src/lib/attributes.js +511 -0
- package/src/lib/constants.js +213 -0
- package/src/lib/content.js +105 -0
- package/src/lib/elements.js +242 -0
- package/src/lib/index.js +20 -0
- package/src/lib/options.js +300 -0
- package/src/lib/utils.js +90 -0
- package/src/lib/whitespace.js +139 -0
- package/src/presets.js +0 -1
- package/src/tokenchain.js +1 -1
- package/dist/types/utils.d.ts +0 -2
- package/dist/types/utils.d.ts.map +0 -1
package/src/htmlparser.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
/*
|
|
9
|
-
*
|
|
9
|
+
* Use like so:
|
|
10
|
+
*
|
|
10
11
|
* HTMLParser(htmlString, {
|
|
11
12
|
* start: function(tag, attrs, unary) {},
|
|
12
13
|
* end: function(tag) {},
|
|
@@ -25,11 +26,11 @@ class CaseInsensitiveSet extends Set {
|
|
|
25
26
|
const singleAttrIdentifier = /([^\s"'<>/=]+)/;
|
|
26
27
|
const singleAttrAssigns = [/=/];
|
|
27
28
|
const singleAttrValues = [
|
|
28
|
-
//
|
|
29
|
+
// Attr value double quotes
|
|
29
30
|
/"([^"]*)"+/.source,
|
|
30
|
-
//
|
|
31
|
+
// Attr value, single quotes
|
|
31
32
|
/'([^']*)'+/.source,
|
|
32
|
-
//
|
|
33
|
+
// Attr value, no quotes
|
|
33
34
|
/([^ \t\n\f\r"'`=<>]+)/.source
|
|
34
35
|
];
|
|
35
36
|
// https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
|
|
@@ -58,18 +59,17 @@ const empty = new CaseInsensitiveSet(['area', 'base', 'basefont', 'br', 'col', '
|
|
|
58
59
|
// Inline elements
|
|
59
60
|
const inline = new CaseInsensitiveSet(['a', 'abbr', 'acronym', 'applet', 'b', 'basefont', 'bdo', 'big', 'br', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'map', 'noscript', 'object', 'q', 's', 'samp', 'script', 'select', 'selectedcontent', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'svg', 'textarea', 'tt', 'u', 'var']);
|
|
60
61
|
|
|
61
|
-
// Elements that you can, intentionally, leave open
|
|
62
|
-
// (and which close themselves)
|
|
62
|
+
// Elements that you can, intentionally, leave open (and which close themselves)
|
|
63
63
|
const closeSelf = new CaseInsensitiveSet(['colgroup', 'dd', 'dt', 'li', 'option', 'p', 'td', 'tfoot', 'th', 'thead', 'tr', 'source']);
|
|
64
64
|
|
|
65
|
-
// Attributes that have their values filled in disabled='disabled'
|
|
65
|
+
// Attributes that have their values filled in `disabled='disabled'`
|
|
66
66
|
const fillAttrs = new CaseInsensitiveSet(['checked', 'compact', 'declare', 'defer', 'disabled', 'ismap', 'multiple', 'nohref', 'noresize', 'noshade', 'nowrap', 'readonly', 'selected']);
|
|
67
67
|
|
|
68
68
|
// Special elements (can contain anything)
|
|
69
69
|
const special = new CaseInsensitiveSet(['script', 'style']);
|
|
70
70
|
|
|
71
|
-
// HTML elements https://html.spec.whatwg.org/multipage/indices.html#elements-3
|
|
72
|
-
// Phrasing
|
|
71
|
+
// HTML elements, https://html.spec.whatwg.org/multipage/indices.html#elements-3
|
|
72
|
+
// Phrasing content, https://html.spec.whatwg.org/multipage/dom.html#phrasing-content
|
|
73
73
|
const nonPhrasing = new CaseInsensitiveSet(['address', 'article', 'aside', 'base', 'blockquote', 'body', 'caption', 'col', 'colgroup', 'dd', 'details', 'dialog', 'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'legend', 'li', 'menuitem', 'meta', 'ol', 'optgroup', 'option', 'param', 'rp', 'rt', 'source', 'style', 'summary', 'tbody', 'td', 'tfoot', 'th', 'thead', 'title', 'tr', 'track', 'ul']);
|
|
74
74
|
|
|
75
75
|
const reCache = {};
|
|
@@ -191,7 +191,7 @@ export class HTMLParser {
|
|
|
191
191
|
|
|
192
192
|
if (conditionalEnd >= 0) {
|
|
193
193
|
if (handler.comment) {
|
|
194
|
-
await handler.comment(html.substring(2, conditionalEnd + 1), true /*
|
|
194
|
+
await handler.comment(html.substring(2, conditionalEnd + 1), true /* Non-standard */);
|
|
195
195
|
}
|
|
196
196
|
advance(conditionalEnd + 2);
|
|
197
197
|
prevTag = '';
|
|
@@ -368,7 +368,7 @@ export class HTMLParser {
|
|
|
368
368
|
attr = [];
|
|
369
369
|
attr[0] = fullAttr;
|
|
370
370
|
attr[baseIndex] = manualMatch[1]; // Attribute name
|
|
371
|
-
attr[baseIndex + 1] = '='; // customAssign (falls back to “=” for huge attributes)
|
|
371
|
+
attr[baseIndex + 1] = '='; // `customAssign` (falls back to “=” for huge attributes)
|
|
372
372
|
const value = input.slice(manualMatch[0].length + 1, closeQuote);
|
|
373
373
|
// Place value at correct index based on quote type
|
|
374
374
|
if (quoteChar === '"') {
|
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
// Imports
|
|
2
|
+
|
|
3
|
+
import { decodeHTMLStrict } from 'entities';
|
|
4
|
+
import {
|
|
5
|
+
RE_CONDITIONAL_COMMENT,
|
|
6
|
+
RE_EVENT_ATTR_DEFAULT,
|
|
7
|
+
RE_CAN_REMOVE_ATTR_QUOTES,
|
|
8
|
+
RE_AMP_ENTITY,
|
|
9
|
+
generalDefaults,
|
|
10
|
+
tagDefaults,
|
|
11
|
+
executableScriptsMimetypes,
|
|
12
|
+
keepScriptsMimetypes,
|
|
13
|
+
isSimpleBoolean,
|
|
14
|
+
isBooleanValue,
|
|
15
|
+
srcsetTags,
|
|
16
|
+
reEmptyAttribute
|
|
17
|
+
} from './constants.js';
|
|
18
|
+
import { trimWhitespace, collapseWhitespaceAll } from './whitespace.js';
|
|
19
|
+
import { shouldMinifyInnerHTML } from './options.js';
|
|
20
|
+
|
|
21
|
+
// Validators
|
|
22
|
+
|
|
23
|
+
function isConditionalComment(text) {
|
|
24
|
+
return RE_CONDITIONAL_COMMENT.test(text);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isIgnoredComment(text, options) {
|
|
28
|
+
for (let i = 0, len = options.ignoreCustomComments.length; i < len; i++) {
|
|
29
|
+
if (options.ignoreCustomComments[i].test(text)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isEventAttribute(attrName, options) {
|
|
37
|
+
const patterns = options.customEventAttributes;
|
|
38
|
+
if (patterns) {
|
|
39
|
+
for (let i = patterns.length; i--;) {
|
|
40
|
+
if (patterns[i].test(attrName)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return RE_EVENT_ATTR_DEFAULT.test(attrName);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function canRemoveAttributeQuotes(value) {
|
|
50
|
+
// https://mathiasbynens.be/notes/unquoted-attribute-values
|
|
51
|
+
return RE_CAN_REMOVE_ATTR_QUOTES.test(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function attributesInclude(attributes, attribute) {
|
|
55
|
+
for (let i = attributes.length; i--;) {
|
|
56
|
+
if (attributes[i].name.toLowerCase() === attribute) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isAttributeRedundant(tag, attrName, attrValue, attrs) {
|
|
64
|
+
attrValue = attrValue ? trimWhitespace(attrValue.toLowerCase()) : '';
|
|
65
|
+
|
|
66
|
+
// Legacy attributes
|
|
67
|
+
if (tag === 'script' && attrName === 'language' && attrValue === 'javascript') {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
if (tag === 'script' && attrName === 'charset' && !attributesInclude(attrs, 'src')) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
if (tag === 'a' && attrName === 'name' && attributesInclude(attrs, 'id')) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check general defaults
|
|
78
|
+
if (generalDefaults[attrName] === attrValue) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check tag-specific defaults
|
|
83
|
+
return tagDefaults[tag]?.[attrName] === attrValue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isScriptTypeAttribute(attrValue = '') {
|
|
87
|
+
attrValue = trimWhitespace(attrValue.split(/;/, 2)[0]).toLowerCase();
|
|
88
|
+
return attrValue === '' || executableScriptsMimetypes.has(attrValue);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function keepScriptTypeAttribute(attrValue = '') {
|
|
92
|
+
attrValue = trimWhitespace(attrValue.split(/;/, 2)[0]).toLowerCase();
|
|
93
|
+
return keepScriptsMimetypes.has(attrValue);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isExecutableScript(tag, attrs) {
|
|
97
|
+
if (tag !== 'script') {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
101
|
+
const attrName = attrs[i].name.toLowerCase();
|
|
102
|
+
if (attrName === 'type') {
|
|
103
|
+
return isScriptTypeAttribute(attrs[i].value);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isStyleLinkTypeAttribute(attrValue = '') {
|
|
110
|
+
attrValue = trimWhitespace(attrValue).toLowerCase();
|
|
111
|
+
return attrValue === '' || attrValue === 'text/css';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isStyleSheet(tag, attrs) {
|
|
115
|
+
if (tag !== 'style') {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
119
|
+
const attrName = attrs[i].name.toLowerCase();
|
|
120
|
+
if (attrName === 'type') {
|
|
121
|
+
return isStyleLinkTypeAttribute(attrs[i].value);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isBooleanAttribute(attrName, attrValue) {
|
|
128
|
+
return isSimpleBoolean.has(attrName) || (attrName === 'draggable' && !isBooleanValue.has(attrValue));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isUriTypeAttribute(attrName, tag) {
|
|
132
|
+
return (
|
|
133
|
+
(/^(?:a|area|link|base)$/.test(tag) && attrName === 'href') ||
|
|
134
|
+
(tag === 'img' && /^(?:src|longdesc|usemap)$/.test(attrName)) ||
|
|
135
|
+
(tag === 'object' && /^(?:classid|codebase|data|usemap)$/.test(attrName)) ||
|
|
136
|
+
(tag === 'q' && attrName === 'cite') ||
|
|
137
|
+
(tag === 'blockquote' && attrName === 'cite') ||
|
|
138
|
+
((tag === 'ins' || tag === 'del') && attrName === 'cite') ||
|
|
139
|
+
(tag === 'form' && attrName === 'action') ||
|
|
140
|
+
(tag === 'input' && (attrName === 'src' || attrName === 'usemap')) ||
|
|
141
|
+
(tag === 'head' && attrName === 'profile') ||
|
|
142
|
+
(tag === 'script' && (attrName === 'src' || attrName === 'for'))
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isNumberTypeAttribute(attrName, tag) {
|
|
147
|
+
return (
|
|
148
|
+
(/^(?:a|area|object|button)$/.test(tag) && attrName === 'tabindex') ||
|
|
149
|
+
(tag === 'input' && (attrName === 'maxlength' || attrName === 'tabindex')) ||
|
|
150
|
+
(tag === 'select' && (attrName === 'size' || attrName === 'tabindex')) ||
|
|
151
|
+
(tag === 'textarea' && /^(?:rows|cols|tabindex)$/.test(attrName)) ||
|
|
152
|
+
(tag === 'colgroup' && attrName === 'span') ||
|
|
153
|
+
(tag === 'col' && attrName === 'span') ||
|
|
154
|
+
((tag === 'th' || tag === 'td') && (attrName === 'rowspan' || attrName === 'colspan'))
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isLinkType(tag, attrs, value) {
|
|
159
|
+
if (tag !== 'link') return false;
|
|
160
|
+
const needle = String(value).toLowerCase();
|
|
161
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
162
|
+
if (attrs[i].name.toLowerCase() === 'rel') {
|
|
163
|
+
const tokens = String(attrs[i].value).toLowerCase().split(/\s+/);
|
|
164
|
+
if (tokens.includes(needle)) return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isMediaQuery(tag, attrs, attrName) {
|
|
171
|
+
return attrName === 'media' && (isLinkType(tag, attrs, 'stylesheet') || isStyleSheet(tag, attrs));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isSrcset(attrName, tag) {
|
|
175
|
+
return attrName === 'srcset' && srcsetTags.has(tag);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isMetaViewport(tag, attrs) {
|
|
179
|
+
if (tag !== 'meta') {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
183
|
+
if (attrs[i].name === 'name' && attrs[i].value === 'viewport') {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isContentSecurityPolicy(tag, attrs) {
|
|
191
|
+
if (tag !== 'meta') {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
195
|
+
if (attrs[i].name.toLowerCase() === 'http-equiv' && attrs[i].value.toLowerCase() === 'content-security-policy') {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function canDeleteEmptyAttribute(tag, attrName, attrValue, options) {
|
|
203
|
+
const isValueEmpty = !attrValue || /^\s*$/.test(attrValue);
|
|
204
|
+
if (!isValueEmpty) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
if (typeof options.removeEmptyAttributes === 'function') {
|
|
208
|
+
return options.removeEmptyAttributes(attrName, tag);
|
|
209
|
+
}
|
|
210
|
+
return (tag === 'input' && attrName === 'value') || reEmptyAttribute.test(attrName);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function hasAttrName(name, attrs) {
|
|
214
|
+
for (let i = attrs.length - 1; i >= 0; i--) {
|
|
215
|
+
if (attrs[i].name === name) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Cleaners
|
|
223
|
+
|
|
224
|
+
async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTMLSelf) {
|
|
225
|
+
// Apply early whitespace normalization if enabled
|
|
226
|
+
// Preserves special spaces (non-breaking space, hair space, etc.) for consistency with `collapseWhitespace`
|
|
227
|
+
if (options.collapseAttributeWhitespace) {
|
|
228
|
+
attrValue = attrValue.replace(/[ \n\r\t\f]+/g, ' ').replace(/^[ \n\r\t\f]+|[ \n\r\t\f]+$/g, '');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (isEventAttribute(attrName, options)) {
|
|
232
|
+
attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, '');
|
|
233
|
+
try {
|
|
234
|
+
return await options.minifyJS(attrValue, true);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
if (!options.continueOnMinifyError) {
|
|
237
|
+
throw err;
|
|
238
|
+
}
|
|
239
|
+
options.log && options.log(err);
|
|
240
|
+
return attrValue;
|
|
241
|
+
}
|
|
242
|
+
} else if (attrName === 'class') {
|
|
243
|
+
attrValue = trimWhitespace(attrValue);
|
|
244
|
+
if (options.sortClassName) {
|
|
245
|
+
attrValue = options.sortClassName(attrValue);
|
|
246
|
+
} else {
|
|
247
|
+
attrValue = collapseWhitespaceAll(attrValue);
|
|
248
|
+
}
|
|
249
|
+
return attrValue;
|
|
250
|
+
} else if (isUriTypeAttribute(attrName, tag)) {
|
|
251
|
+
attrValue = trimWhitespace(attrValue);
|
|
252
|
+
if (isLinkType(tag, attrs, 'canonical')) {
|
|
253
|
+
return attrValue;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
const out = await options.minifyURLs(attrValue);
|
|
257
|
+
return typeof out === 'string' ? out : attrValue;
|
|
258
|
+
} catch (err) {
|
|
259
|
+
if (!options.continueOnMinifyError) {
|
|
260
|
+
throw err;
|
|
261
|
+
}
|
|
262
|
+
options.log && options.log(err);
|
|
263
|
+
return attrValue;
|
|
264
|
+
}
|
|
265
|
+
} else if (isNumberTypeAttribute(attrName, tag)) {
|
|
266
|
+
return trimWhitespace(attrValue);
|
|
267
|
+
} else if (attrName === 'style') {
|
|
268
|
+
attrValue = trimWhitespace(attrValue);
|
|
269
|
+
if (attrValue) {
|
|
270
|
+
if (attrValue.endsWith(';') && !/&#?[0-9a-zA-Z]+;$/.test(attrValue)) {
|
|
271
|
+
attrValue = attrValue.replace(/\s*;$/, ';');
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
attrValue = await options.minifyCSS(attrValue, 'inline');
|
|
275
|
+
} catch (err) {
|
|
276
|
+
if (!options.continueOnMinifyError) {
|
|
277
|
+
throw err;
|
|
278
|
+
}
|
|
279
|
+
options.log && options.log(err);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return attrValue;
|
|
283
|
+
} else if (isSrcset(attrName, tag)) {
|
|
284
|
+
// https://html.spec.whatwg.org/multipage/embedded-content.html#attr-img-srcset
|
|
285
|
+
attrValue = (await Promise.all(trimWhitespace(attrValue).split(/\s*,\s*/).map(async function (candidate) {
|
|
286
|
+
let url = candidate;
|
|
287
|
+
let descriptor = '';
|
|
288
|
+
const match = candidate.match(/\s+([1-9][0-9]*w|[0-9]+(?:\.[0-9]+)?x)$/);
|
|
289
|
+
if (match) {
|
|
290
|
+
url = url.slice(0, -match[0].length);
|
|
291
|
+
const num = +match[1].slice(0, -1);
|
|
292
|
+
const suffix = match[1].slice(-1);
|
|
293
|
+
if (num !== 1 || suffix !== 'x') {
|
|
294
|
+
descriptor = ' ' + num + suffix;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
const out = await options.minifyURLs(url);
|
|
299
|
+
return (typeof out === 'string' ? out : url) + descriptor;
|
|
300
|
+
} catch (err) {
|
|
301
|
+
if (!options.continueOnMinifyError) {
|
|
302
|
+
throw err;
|
|
303
|
+
}
|
|
304
|
+
options.log && options.log(err);
|
|
305
|
+
return url + descriptor;
|
|
306
|
+
}
|
|
307
|
+
}))).join(', ');
|
|
308
|
+
} else if (isMetaViewport(tag, attrs) && attrName === 'content') {
|
|
309
|
+
attrValue = attrValue.replace(/\s+/g, '').replace(/[0-9]+\.[0-9]+/g, function (numString) {
|
|
310
|
+
// 0.90000 → 0.9
|
|
311
|
+
// 1.0 → 1
|
|
312
|
+
// 1.0001 → 1.0001 (unchanged)
|
|
313
|
+
return (+numString).toString();
|
|
314
|
+
});
|
|
315
|
+
} else if (isContentSecurityPolicy(tag, attrs) && attrName.toLowerCase() === 'content') {
|
|
316
|
+
return collapseWhitespaceAll(attrValue);
|
|
317
|
+
} else if (options.customAttrCollapse && options.customAttrCollapse.test(attrName)) {
|
|
318
|
+
attrValue = trimWhitespace(attrValue.replace(/ ?[\n\r]+ ?/g, '').replace(/\s{2,}/g, options.conservativeCollapse ? ' ' : ''));
|
|
319
|
+
} else if (tag === 'script' && attrName === 'type') {
|
|
320
|
+
attrValue = trimWhitespace(attrValue.replace(/\s*;\s*/g, ';'));
|
|
321
|
+
} else if (isMediaQuery(tag, attrs, attrName)) {
|
|
322
|
+
attrValue = trimWhitespace(attrValue);
|
|
323
|
+
try {
|
|
324
|
+
return await options.minifyCSS(attrValue, 'media');
|
|
325
|
+
} catch (err) {
|
|
326
|
+
if (!options.continueOnMinifyError) {
|
|
327
|
+
throw err;
|
|
328
|
+
}
|
|
329
|
+
options.log && options.log(err);
|
|
330
|
+
return attrValue;
|
|
331
|
+
}
|
|
332
|
+
} else if (tag === 'iframe' && attrName === 'srcdoc') {
|
|
333
|
+
// Recursively minify HTML content within `srcdoc` attribute
|
|
334
|
+
// Fast-path: Skip if nothing would change
|
|
335
|
+
if (!shouldMinifyInnerHTML(options)) {
|
|
336
|
+
return attrValue;
|
|
337
|
+
}
|
|
338
|
+
return minifyHTMLSelf(attrValue, options, true);
|
|
339
|
+
}
|
|
340
|
+
return attrValue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Choose appropriate quote character for an attribute value
|
|
345
|
+
* @param {string} attrValue - The attribute value
|
|
346
|
+
* @param {Object} options - Minifier options
|
|
347
|
+
* @returns {string} The chosen quote character (`"` or `'`)
|
|
348
|
+
*/
|
|
349
|
+
function chooseAttributeQuote(attrValue, options) {
|
|
350
|
+
if (typeof options.quoteCharacter !== 'undefined') {
|
|
351
|
+
return options.quoteCharacter === '\'' ? '\'' : '"';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Count quotes in a single pass
|
|
355
|
+
let apos = 0, quot = 0;
|
|
356
|
+
for (let i = 0; i < attrValue.length; i++) {
|
|
357
|
+
if (attrValue[i] === "'") apos++;
|
|
358
|
+
else if (attrValue[i] === '"') quot++;
|
|
359
|
+
}
|
|
360
|
+
return apos < quot ? '\'' : '"';
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
364
|
+
const attrName = options.name(attr.name);
|
|
365
|
+
let attrValue = attr.value;
|
|
366
|
+
|
|
367
|
+
if (options.decodeEntities && attrValue) {
|
|
368
|
+
// Fast path: Only decode when entities are present
|
|
369
|
+
if (attrValue.indexOf('&') !== -1) {
|
|
370
|
+
attrValue = decodeHTMLStrict(attrValue);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if ((options.removeRedundantAttributes &&
|
|
375
|
+
isAttributeRedundant(tag, attrName, attrValue, attrs)) ||
|
|
376
|
+
(options.removeScriptTypeAttributes && tag === 'script' &&
|
|
377
|
+
attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue)) ||
|
|
378
|
+
(options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
|
|
379
|
+
attrName === 'type' && isStyleLinkTypeAttribute(attrValue))) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (attrValue) {
|
|
384
|
+
attrValue = await cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTML);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (options.removeEmptyAttributes &&
|
|
388
|
+
canDeleteEmptyAttribute(tag, attrName, attrValue, options)) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (options.decodeEntities && attrValue && attrValue.indexOf('&') !== -1) {
|
|
393
|
+
attrValue = attrValue.replace(RE_AMP_ENTITY, '&$1');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
attr,
|
|
398
|
+
name: attrName,
|
|
399
|
+
value: attrValue
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
404
|
+
const attrName = normalized.name;
|
|
405
|
+
let attrValue = normalized.value;
|
|
406
|
+
const attr = normalized.attr;
|
|
407
|
+
let attrQuote = attr.quote;
|
|
408
|
+
let attrFragment;
|
|
409
|
+
let emittedAttrValue;
|
|
410
|
+
|
|
411
|
+
if (typeof attrValue !== 'undefined' && (!options.removeAttributeQuotes ||
|
|
412
|
+
attrValue.indexOf(uidAttr) !== -1 || !canRemoveAttributeQuotes(attrValue))) {
|
|
413
|
+
// Determine the appropriate quote character
|
|
414
|
+
if (!options.preventAttributesEscaping) {
|
|
415
|
+
// Normal mode: choose quotes and escape
|
|
416
|
+
attrQuote = chooseAttributeQuote(attrValue, options);
|
|
417
|
+
if (attrQuote === '"') {
|
|
418
|
+
attrValue = attrValue.replace(/"/g, '"');
|
|
419
|
+
} else {
|
|
420
|
+
attrValue = attrValue.replace(/'/g, ''');
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
// `preventAttributesEscaping` mode: choose safe quotes but don't escape
|
|
424
|
+
// except when both quote types are present—then escape to prevent invalid HTML
|
|
425
|
+
const hasDoubleQuote = attrValue.indexOf('"') !== -1;
|
|
426
|
+
const hasSingleQuote = attrValue.indexOf("'") !== -1;
|
|
427
|
+
|
|
428
|
+
// Both quote types present: Escaping is required to guarantee valid HTML delimiter matching
|
|
429
|
+
if (hasDoubleQuote && hasSingleQuote) {
|
|
430
|
+
attrQuote = chooseAttributeQuote(attrValue, options);
|
|
431
|
+
if (attrQuote === '"') {
|
|
432
|
+
attrValue = attrValue.replace(/"/g, '"');
|
|
433
|
+
} else {
|
|
434
|
+
attrValue = attrValue.replace(/'/g, ''');
|
|
435
|
+
}
|
|
436
|
+
// Auto quote selection: Prefer the opposite quote type when value contains one quote type, default to double quotes when none present
|
|
437
|
+
} else if (typeof options.quoteCharacter === 'undefined') {
|
|
438
|
+
if (attrQuote === '"' && hasDoubleQuote && !hasSingleQuote) {
|
|
439
|
+
attrQuote = "'";
|
|
440
|
+
} else if (attrQuote === "'" && hasSingleQuote && !hasDoubleQuote) {
|
|
441
|
+
attrQuote = '"';
|
|
442
|
+
// Fallback for invalid/unsupported attrQuote values (not `"`, `'`, or empty string): Choose safe default based on value content
|
|
443
|
+
} else if (attrQuote !== '"' && attrQuote !== "'" && attrQuote !== '') {
|
|
444
|
+
if (hasSingleQuote && !hasDoubleQuote) {
|
|
445
|
+
attrQuote = '"';
|
|
446
|
+
} else if (hasDoubleQuote && !hasSingleQuote) {
|
|
447
|
+
attrQuote = "'";
|
|
448
|
+
} else {
|
|
449
|
+
attrQuote = '"';
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
attrQuote = options.quoteCharacter === '\'' ? '\'' : '"';
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
emittedAttrValue = attrQuote + attrValue + attrQuote;
|
|
457
|
+
if (!isLast && !options.removeTagWhitespace) {
|
|
458
|
+
emittedAttrValue += ' ';
|
|
459
|
+
}
|
|
460
|
+
} else if (isLast && !hasUnarySlash) {
|
|
461
|
+
// Last attribute in a non-self-closing tag: no space needed
|
|
462
|
+
emittedAttrValue = attrValue;
|
|
463
|
+
} else {
|
|
464
|
+
// Not last attribute, or is a self-closing tag: add space
|
|
465
|
+
emittedAttrValue = attrValue + ' ';
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (typeof attrValue === 'undefined' || (options.collapseBooleanAttributes &&
|
|
469
|
+
isBooleanAttribute(attrName.toLowerCase(), (attrValue || '').toLowerCase()))) {
|
|
470
|
+
attrFragment = attrName;
|
|
471
|
+
if (!isLast) {
|
|
472
|
+
attrFragment += ' ';
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
attrFragment = attrName + attr.customAssign + emittedAttrValue;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return attr.customOpen + attrFragment + attr.customClose;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Exports
|
|
482
|
+
|
|
483
|
+
export {
|
|
484
|
+
// Validators
|
|
485
|
+
isConditionalComment,
|
|
486
|
+
isIgnoredComment,
|
|
487
|
+
isEventAttribute,
|
|
488
|
+
canRemoveAttributeQuotes,
|
|
489
|
+
attributesInclude,
|
|
490
|
+
isAttributeRedundant,
|
|
491
|
+
isScriptTypeAttribute,
|
|
492
|
+
keepScriptTypeAttribute,
|
|
493
|
+
isExecutableScript,
|
|
494
|
+
isStyleLinkTypeAttribute,
|
|
495
|
+
isStyleSheet,
|
|
496
|
+
isBooleanAttribute,
|
|
497
|
+
isUriTypeAttribute,
|
|
498
|
+
isNumberTypeAttribute,
|
|
499
|
+
isLinkType,
|
|
500
|
+
isMediaQuery,
|
|
501
|
+
isSrcset,
|
|
502
|
+
isMetaViewport,
|
|
503
|
+
isContentSecurityPolicy,
|
|
504
|
+
canDeleteEmptyAttribute,
|
|
505
|
+
hasAttrName,
|
|
506
|
+
|
|
507
|
+
// Cleaners
|
|
508
|
+
cleanAttributeValue,
|
|
509
|
+
normalizeAttr,
|
|
510
|
+
buildAttr
|
|
511
|
+
};
|