html-minifier-next 6.2.8 → 6.2.9
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 +35 -1
- package/dist/htmlminifier.cjs +882 -557
- package/dist/types/htmlminifier.d.ts +77 -60
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/htmlparser.d.ts +19 -3
- package/dist/types/htmlparser.d.ts.map +1 -1
- package/dist/types/lib/attributes.d.ts +137 -23
- package/dist/types/lib/attributes.d.ts.map +1 -1
- package/dist/types/lib/content.d.ts +37 -5
- package/dist/types/lib/content.d.ts.map +1 -1
- package/dist/types/lib/elements.d.ts +29 -4
- package/dist/types/lib/elements.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts +17 -14
- package/dist/types/lib/options.d.ts.map +1 -1
- package/dist/types/lib/utils.d.ts +28 -11
- package/dist/types/lib/utils.d.ts.map +1 -1
- package/dist/types/lib/whitespace.d.ts +40 -6
- package/dist/types/lib/whitespace.d.ts.map +1 -1
- package/dist/types/tokenchain.d.ts +17 -3
- package/dist/types/tokenchain.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/htmlminifier.js +405 -346
- package/src/htmlparser.js +101 -48
- package/src/lib/attributes.js +166 -50
- package/src/lib/content.js +28 -12
- package/src/lib/elements.js +30 -7
- package/src/lib/options.js +55 -42
- package/src/lib/utils.js +26 -7
- package/src/lib/whitespace.js +30 -11
- package/src/presets.js +1 -1
- package/src/tokenchain.js +30 -17
package/src/lib/attributes.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// Imports
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
RE_EVENT_ATTR_DEFAULT,
|
|
5
3
|
RE_CAN_REMOVE_ATTR_QUOTES,
|
|
@@ -21,8 +19,16 @@ import { trimWhitespace, collapseWhitespaceAll } from './whitespace.js';
|
|
|
21
19
|
import { shouldMinifyInnerHTML } from './options.js';
|
|
22
20
|
import { identity, isThenable } from './utils.js';
|
|
23
21
|
|
|
22
|
+
// Type definitions
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {{ name: string, value?: string | undefined, quote?: string, customAssign?: string, customOpen?: string, customClose?: string }} HTMLAttribute
|
|
26
|
+
* Internal counterpart of the public typedef in `htmlminifier.js`—keep in sync.
|
|
27
|
+
*/
|
|
28
|
+
|
|
24
29
|
// Lazy-load entities (used for `decodeEntities` and event-handler attribute decode before `minifyJS`)
|
|
25
30
|
|
|
31
|
+
/** @type {Promise<Function> | undefined} */
|
|
26
32
|
let decodeHTMLStrictPromise;
|
|
27
33
|
async function getDecodeHTMLStrict() {
|
|
28
34
|
if (!decodeHTMLStrictPromise) {
|
|
@@ -33,20 +39,30 @@ async function getDecodeHTMLStrict() {
|
|
|
33
39
|
|
|
34
40
|
// Validators
|
|
35
41
|
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} text
|
|
44
|
+
* @param {{ignoreCustomComments: RegExp[]}} options
|
|
45
|
+
*/
|
|
36
46
|
function isIgnoredComment(text, options) {
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
// @@ Optimize: `Array.isArray(options.ignoreCustomComments)` runs on every comment node; it could be eliminated once `parseRegExpArray` is tightened to coerce non-arrays to `[]` at setup time
|
|
48
|
+
if (!Array.isArray(options.ignoreCustomComments)) return false;
|
|
49
|
+
for (const pattern of options.ignoreCustomComments) {
|
|
50
|
+
if (pattern.test(text)) {
|
|
39
51
|
return true;
|
|
40
52
|
}
|
|
41
53
|
}
|
|
42
54
|
return false;
|
|
43
55
|
}
|
|
44
56
|
|
|
57
|
+
/**
|
|
58
|
+
* @param {string} attrName
|
|
59
|
+
* @param {{customEventAttributes?: RegExp[]}} options
|
|
60
|
+
*/
|
|
45
61
|
function isEventAttribute(attrName, options) {
|
|
46
62
|
const patterns = options.customEventAttributes;
|
|
47
63
|
if (patterns) {
|
|
48
|
-
for (
|
|
49
|
-
if (
|
|
64
|
+
for (const pattern of patterns) {
|
|
65
|
+
if (pattern.test(attrName)) {
|
|
50
66
|
return true;
|
|
51
67
|
}
|
|
52
68
|
}
|
|
@@ -55,14 +71,19 @@ function isEventAttribute(attrName, options) {
|
|
|
55
71
|
return RE_EVENT_ATTR_DEFAULT.test(attrName);
|
|
56
72
|
}
|
|
57
73
|
|
|
74
|
+
/** @param {string} value */
|
|
58
75
|
function canRemoveAttributeQuotes(value) {
|
|
59
76
|
// https://mathiasbynens.be/notes/unquoted-attribute-values
|
|
60
77
|
return RE_CAN_REMOVE_ATTR_QUOTES.test(value);
|
|
61
78
|
}
|
|
62
79
|
|
|
80
|
+
/**
|
|
81
|
+
* @param {HTMLAttribute[]} attributes
|
|
82
|
+
* @param {string} attribute
|
|
83
|
+
*/
|
|
63
84
|
function attributesInclude(attributes, attribute) {
|
|
64
|
-
for (
|
|
65
|
-
if (
|
|
85
|
+
for (const attr of attributes) {
|
|
86
|
+
if (attr.name.toLowerCase() === attribute) {
|
|
66
87
|
return true;
|
|
67
88
|
}
|
|
68
89
|
}
|
|
@@ -73,9 +94,9 @@ function attributesInclude(attributes, attribute) {
|
|
|
73
94
|
* Remove duplicate attributes from an attribute list.
|
|
74
95
|
* Per HTML spec, when an attribute appears multiple times, the first occurrence wins.
|
|
75
96
|
* Duplicate attributes result in invalid HTML, so only the first is kept.
|
|
76
|
-
* @param {
|
|
97
|
+
* @param {HTMLAttribute[]} attrs - Array of attribute objects with `name` property
|
|
77
98
|
* @param {boolean} caseSensitive - Whether to compare names case-sensitively (for XML/SVG)
|
|
78
|
-
* @returns {
|
|
99
|
+
* @returns {HTMLAttribute[]} Deduplicated attribute array (modifies in place and returns)
|
|
79
100
|
*/
|
|
80
101
|
function deduplicateAttributes(attrs, caseSensitive) {
|
|
81
102
|
if (attrs.length < 2) {
|
|
@@ -85,8 +106,7 @@ function deduplicateAttributes(attrs, caseSensitive) {
|
|
|
85
106
|
const seen = new Set();
|
|
86
107
|
let writeIndex = 0;
|
|
87
108
|
|
|
88
|
-
for (
|
|
89
|
-
const attr = attrs[i];
|
|
109
|
+
for (const attr of attrs) {
|
|
90
110
|
const key = caseSensitive ? attr.name : attr.name.toLowerCase();
|
|
91
111
|
|
|
92
112
|
if (!seen.has(key)) {
|
|
@@ -99,6 +119,12 @@ function deduplicateAttributes(attrs, caseSensitive) {
|
|
|
99
119
|
return attrs;
|
|
100
120
|
}
|
|
101
121
|
|
|
122
|
+
/**
|
|
123
|
+
* @param {string} tag
|
|
124
|
+
* @param {string} attrName
|
|
125
|
+
* @param {string} attrValue
|
|
126
|
+
* @param {HTMLAttribute[]} attrs
|
|
127
|
+
*/
|
|
102
128
|
function isAttributeRedundant(tag, attrName, attrValue, attrs) {
|
|
103
129
|
// Fast-path: Check if this element–attribute combination can possibly be redundant
|
|
104
130
|
// before doing expensive string operations
|
|
@@ -132,32 +158,35 @@ function isAttributeRedundant(tag, attrName, attrValue, attrs) {
|
|
|
132
158
|
}
|
|
133
159
|
|
|
134
160
|
// Check general defaults
|
|
135
|
-
if (hasGeneralDefault && generalDefaults[attrName] === attrValue) {
|
|
161
|
+
if (hasGeneralDefault && /** @type {Record<string, string>} */ (generalDefaults)[attrName] === attrValue) {
|
|
136
162
|
return true;
|
|
137
163
|
}
|
|
138
164
|
|
|
139
165
|
// Check tag-specific defaults
|
|
140
|
-
return tagHasDefaults && tagDefaults[tag][attrName] === attrValue;
|
|
166
|
+
return tagHasDefaults && /** @type {Record<string, string>} */ (/** @type {Record<string, unknown>} */ (tagDefaults)[tag])[attrName] === attrValue;
|
|
141
167
|
}
|
|
142
168
|
|
|
143
169
|
function isScriptTypeAttribute(attrValue = '') {
|
|
144
|
-
attrValue = trimWhitespace(attrValue.split(/;/, 2)[0]).toLowerCase();
|
|
170
|
+
attrValue = trimWhitespace(attrValue.split(/;/, 2)[0] ?? '').toLowerCase();
|
|
145
171
|
return attrValue === '' || executableScriptsMimetypes.has(attrValue);
|
|
146
172
|
}
|
|
147
173
|
|
|
148
174
|
function keepScriptTypeAttribute(attrValue = '') {
|
|
149
|
-
attrValue = trimWhitespace(attrValue.split(/;/, 2)[0]).toLowerCase();
|
|
175
|
+
attrValue = trimWhitespace(attrValue.split(/;/, 2)[0] ?? '').toLowerCase();
|
|
150
176
|
return keepScriptsMimetypes.has(attrValue);
|
|
151
177
|
}
|
|
152
178
|
|
|
179
|
+
/**
|
|
180
|
+
* @param {string} tag
|
|
181
|
+
* @param {HTMLAttribute[]} attrs
|
|
182
|
+
*/
|
|
153
183
|
function isExecutableScript(tag, attrs) {
|
|
154
184
|
if (tag !== 'script') {
|
|
155
185
|
return false;
|
|
156
186
|
}
|
|
157
|
-
for (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return isScriptTypeAttribute(attrs[i].value);
|
|
187
|
+
for (const attr of attrs) {
|
|
188
|
+
if (attr.name.toLowerCase() === 'type') {
|
|
189
|
+
return isScriptTypeAttribute(attr.value);
|
|
161
190
|
}
|
|
162
191
|
}
|
|
163
192
|
return true;
|
|
@@ -168,23 +197,30 @@ function isStyleLinkTypeAttribute(attrValue = '') {
|
|
|
168
197
|
return attrValue === '' || attrValue === 'text/css';
|
|
169
198
|
}
|
|
170
199
|
|
|
200
|
+
/**
|
|
201
|
+
* @param {string} tag
|
|
202
|
+
* @param {HTMLAttribute[]} attrs
|
|
203
|
+
*/
|
|
171
204
|
function isStyleElement(tag, attrs) {
|
|
172
205
|
if (tag !== 'style') {
|
|
173
206
|
return false;
|
|
174
207
|
}
|
|
175
|
-
for (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return isStyleLinkTypeAttribute(attrs[i].value);
|
|
208
|
+
for (const attr of attrs) {
|
|
209
|
+
if (attr.name.toLowerCase() === 'type') {
|
|
210
|
+
return isStyleLinkTypeAttribute(attr.value);
|
|
179
211
|
}
|
|
180
212
|
}
|
|
181
213
|
return true;
|
|
182
214
|
}
|
|
183
215
|
|
|
216
|
+
/**
|
|
217
|
+
* @param {string} attrName
|
|
218
|
+
* @param {string} attrValue
|
|
219
|
+
*/
|
|
184
220
|
function isBooleanAttribute(attrName, attrValue) {
|
|
185
221
|
return isSimpleBoolean.has(attrName) ||
|
|
186
222
|
(attrName === 'draggable' && !isBooleanValue.has(attrValue)) ||
|
|
187
|
-
(collapsibleValues.has(attrName) && collapsibleValues.get(attrName)
|
|
223
|
+
(collapsibleValues.has(attrName) && collapsibleValues.get(attrName)?.has(attrValue));
|
|
188
224
|
}
|
|
189
225
|
|
|
190
226
|
const uriTypeAttributes = new Map([
|
|
@@ -204,6 +240,10 @@ const uriTypeAttributes = new Map([
|
|
|
204
240
|
['script', new Set(['src', 'for'])]
|
|
205
241
|
]);
|
|
206
242
|
|
|
243
|
+
/**
|
|
244
|
+
* @param {string} attrName
|
|
245
|
+
* @param {string} tag
|
|
246
|
+
*/
|
|
207
247
|
function isUriTypeAttribute(attrName, tag) {
|
|
208
248
|
const set = uriTypeAttributes.get(tag);
|
|
209
249
|
return set ? set.has(attrName) : false;
|
|
@@ -223,55 +263,87 @@ const numberTypeAttributes = new Map([
|
|
|
223
263
|
['td', new Set(['rowspan', 'colspan'])]
|
|
224
264
|
]);
|
|
225
265
|
|
|
266
|
+
/**
|
|
267
|
+
* @param {string} attrName
|
|
268
|
+
* @param {string} tag
|
|
269
|
+
*/
|
|
226
270
|
function isNumberTypeAttribute(attrName, tag) {
|
|
227
271
|
const set = numberTypeAttributes.get(tag);
|
|
228
272
|
return set ? set.has(attrName) : false;
|
|
229
273
|
}
|
|
230
274
|
|
|
275
|
+
/**
|
|
276
|
+
* @param {string} tag
|
|
277
|
+
* @param {HTMLAttribute[]} attrs
|
|
278
|
+
* @param {string} value
|
|
279
|
+
*/
|
|
231
280
|
function isLinkType(tag, attrs, value) {
|
|
232
281
|
if (tag !== 'link') return false;
|
|
233
282
|
const needle = String(value).toLowerCase();
|
|
234
|
-
for (
|
|
235
|
-
if (
|
|
236
|
-
const tokens = String(
|
|
283
|
+
for (const attr of attrs) {
|
|
284
|
+
if (attr.name.toLowerCase() === 'rel') {
|
|
285
|
+
const tokens = String(attr.value).toLowerCase().split(/\s+/);
|
|
237
286
|
if (tokens.includes(needle)) return true;
|
|
238
287
|
}
|
|
239
288
|
}
|
|
240
289
|
return false;
|
|
241
290
|
}
|
|
242
291
|
|
|
292
|
+
/**
|
|
293
|
+
* @param {string} tag
|
|
294
|
+
* @param {HTMLAttribute[]} attrs
|
|
295
|
+
* @param {string} attrName
|
|
296
|
+
*/
|
|
243
297
|
function isMediaQuery(tag, attrs, attrName) {
|
|
244
298
|
return attrName === 'media' && (isLinkType(tag, attrs, 'stylesheet') || isStyleElement(tag, attrs));
|
|
245
299
|
}
|
|
246
300
|
|
|
301
|
+
/**
|
|
302
|
+
* @param {string} attrName
|
|
303
|
+
* @param {string} tag
|
|
304
|
+
*/
|
|
247
305
|
function isSrcset(attrName, tag) {
|
|
248
306
|
return attrName === 'srcset' && srcsetElements.has(tag);
|
|
249
307
|
}
|
|
250
308
|
|
|
309
|
+
/**
|
|
310
|
+
* @param {string} tag
|
|
311
|
+
* @param {HTMLAttribute[]} attrs
|
|
312
|
+
*/
|
|
251
313
|
function isMetaViewport(tag, attrs) {
|
|
252
314
|
if (tag !== 'meta') {
|
|
253
315
|
return false;
|
|
254
316
|
}
|
|
255
|
-
for (
|
|
256
|
-
if (
|
|
317
|
+
for (const attr of attrs) {
|
|
318
|
+
if (attr.name.toLowerCase() === 'name' && (attr.value || '').toLowerCase() === 'viewport') {
|
|
257
319
|
return true;
|
|
258
320
|
}
|
|
259
321
|
}
|
|
260
322
|
return false;
|
|
261
323
|
}
|
|
262
324
|
|
|
325
|
+
/**
|
|
326
|
+
* @param {string} tag
|
|
327
|
+
* @param {HTMLAttribute[]} attrs
|
|
328
|
+
*/
|
|
263
329
|
function isContentSecurityPolicy(tag, attrs) {
|
|
264
330
|
if (tag !== 'meta') {
|
|
265
331
|
return false;
|
|
266
332
|
}
|
|
267
|
-
for (
|
|
268
|
-
if (
|
|
333
|
+
for (const attr of attrs) {
|
|
334
|
+
if (attr.name.toLowerCase() === 'http-equiv' && (attr.value || '').toLowerCase() === 'content-security-policy') {
|
|
269
335
|
return true;
|
|
270
336
|
}
|
|
271
337
|
}
|
|
272
338
|
return false;
|
|
273
339
|
}
|
|
274
340
|
|
|
341
|
+
/**
|
|
342
|
+
* @param {string} tag
|
|
343
|
+
* @param {string} attrName
|
|
344
|
+
* @param {string | undefined} attrValue
|
|
345
|
+
* @param {{removeEmptyAttributes?: boolean | Function}} options
|
|
346
|
+
*/
|
|
275
347
|
function canDeleteEmptyAttribute(tag, attrName, attrValue, options) {
|
|
276
348
|
const isValueEmpty = !attrValue || attrValue.trim() === '';
|
|
277
349
|
if (!isValueEmpty) {
|
|
@@ -283,9 +355,13 @@ function canDeleteEmptyAttribute(tag, attrName, attrValue, options) {
|
|
|
283
355
|
return (tag === 'input' && attrName === 'value') || reEmptyAttribute.test(attrName);
|
|
284
356
|
}
|
|
285
357
|
|
|
358
|
+
/**
|
|
359
|
+
* @param {string} name
|
|
360
|
+
* @param {HTMLAttribute[]} attrs
|
|
361
|
+
*/
|
|
286
362
|
function hasAttrName(name, attrs) {
|
|
287
|
-
for (
|
|
288
|
-
if (
|
|
363
|
+
for (const attr of attrs) {
|
|
364
|
+
if (attr.name === name) {
|
|
289
365
|
return true;
|
|
290
366
|
}
|
|
291
367
|
}
|
|
@@ -300,6 +376,14 @@ const valueWhitespaceExemptElements = new Set(['button', 'data', 'input', 'optio
|
|
|
300
376
|
|
|
301
377
|
// Returns the cleaned attribute value directly (sync) or as a Promise (async);
|
|
302
378
|
// callers must handle both cases—use `isThenable()` to distinguish
|
|
379
|
+
/**
|
|
380
|
+
* @param {string} tag
|
|
381
|
+
* @param {string} attrName
|
|
382
|
+
* @param {string} attrValue
|
|
383
|
+
* @param {Record<string, any>} options
|
|
384
|
+
* @param {HTMLAttribute[]} attrs
|
|
385
|
+
* @param {Function} minifyHTMLSelf
|
|
386
|
+
*/
|
|
303
387
|
function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTMLSelf) {
|
|
304
388
|
const isEventAttr = isEventAttribute(attrName, options);
|
|
305
389
|
|
|
@@ -323,9 +407,9 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
|
|
|
323
407
|
return getDecodeHTMLStrict().then(decode => {
|
|
324
408
|
const decoded = decode(attrValue);
|
|
325
409
|
const result = options.minifyJS(decoded, true);
|
|
326
|
-
const reEncode = v => (v && v.indexOf('&') !== -1) ? v.replace(RE_AMP_ENTITY, '&$1') : v;
|
|
410
|
+
const reEncode = (/** @type {string} */ v) => (v && v.indexOf('&') !== -1) ? v.replace(RE_AMP_ENTITY, '&$1') : v;
|
|
327
411
|
if (isThenable(result)) {
|
|
328
|
-
return result.then(reEncode, err => {
|
|
412
|
+
return result.then(reEncode, (/** @type {Error} */ err) => {
|
|
329
413
|
if (!options.continueOnMinifyError) throw err;
|
|
330
414
|
options.log && options.log(err);
|
|
331
415
|
return attrValue;
|
|
@@ -336,7 +420,7 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
|
|
|
336
420
|
}
|
|
337
421
|
const result = options.minifyJS(attrValue, true);
|
|
338
422
|
if (isThenable(result)) {
|
|
339
|
-
return result.catch(err => {
|
|
423
|
+
return result.catch((/** @type {Error} */ err) => {
|
|
340
424
|
if (!options.continueOnMinifyError) throw err;
|
|
341
425
|
options.log && options.log(err);
|
|
342
426
|
return attrValue;
|
|
@@ -363,8 +447,8 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
|
|
|
363
447
|
const result = options.minifyURLs(attrValue);
|
|
364
448
|
if (isThenable(result)) {
|
|
365
449
|
return result
|
|
366
|
-
.then(out => typeof out === 'string' ? out : attrValue)
|
|
367
|
-
.catch(err => {
|
|
450
|
+
.then((/** @type {unknown} */ out) => typeof out === 'string' ? out : attrValue)
|
|
451
|
+
.catch((/** @type {Error} */ err) => {
|
|
368
452
|
if (!options.continueOnMinifyError) throw err;
|
|
369
453
|
options.log && options.log(err);
|
|
370
454
|
return attrValue;
|
|
@@ -387,13 +471,13 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
|
|
|
387
471
|
const cssResult = options.minifyCSS(attrValue, 'inline');
|
|
388
472
|
if (isThenable(cssResult)) {
|
|
389
473
|
return cssResult
|
|
390
|
-
.then(minified => {
|
|
474
|
+
.then((/** @type {string} */ minified) => {
|
|
391
475
|
// After minification, check if CSS consists entirely of invalid properties (no values)
|
|
392
476
|
// I.e., `color:` or `margin:;padding:` should be treated as empty
|
|
393
477
|
if (minified && /^(?:[a-z-]+:[;\s]*)+$/i.test(minified)) return '';
|
|
394
478
|
return minified;
|
|
395
479
|
})
|
|
396
|
-
.catch(err => {
|
|
480
|
+
.catch((/** @type {Error} */ err) => {
|
|
397
481
|
if (!options.continueOnMinifyError) throw err;
|
|
398
482
|
options.log && options.log(err);
|
|
399
483
|
return originalAttrValue;
|
|
@@ -415,8 +499,9 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
|
|
|
415
499
|
const match = candidate.match(/\s+([1-9][0-9]*w|[0-9]+(?:\.[0-9]+)?x)$/);
|
|
416
500
|
if (match) {
|
|
417
501
|
url = url.slice(0, -match[0].length);
|
|
418
|
-
const
|
|
419
|
-
const
|
|
502
|
+
const group = match[1] ?? '';
|
|
503
|
+
const num = +group.slice(0, -1);
|
|
504
|
+
const suffix = group.slice(-1);
|
|
420
505
|
if (num !== 1 || suffix !== 'x') {
|
|
421
506
|
descriptor = ' ' + num + suffix;
|
|
422
507
|
}
|
|
@@ -424,8 +509,8 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
|
|
|
424
509
|
const out = options.minifyURLs(url);
|
|
425
510
|
if (isThenable(out)) {
|
|
426
511
|
return out
|
|
427
|
-
.then(result => (typeof result === 'string' ? result : url) + descriptor)
|
|
428
|
-
.catch(err => {
|
|
512
|
+
.then((/** @type {unknown} */ result) => (typeof result === 'string' ? result : url) + descriptor)
|
|
513
|
+
.catch((/** @type {Error} */ err) => {
|
|
429
514
|
if (!options.continueOnMinifyError) throw err;
|
|
430
515
|
options.log && options.log(err);
|
|
431
516
|
return url + descriptor;
|
|
@@ -470,7 +555,7 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
|
|
|
470
555
|
const originalAttrValue = attrValue;
|
|
471
556
|
const cssResult = options.minifyCSS(attrValue, 'media');
|
|
472
557
|
if (isThenable(cssResult)) {
|
|
473
|
-
return cssResult.catch(err => {
|
|
558
|
+
return cssResult.catch((/** @type {Error} */ err) => {
|
|
474
559
|
if (!options.continueOnMinifyError) throw err;
|
|
475
560
|
options.log && options.log(err);
|
|
476
561
|
return originalAttrValue;
|
|
@@ -494,7 +579,7 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
|
|
|
494
579
|
/**
|
|
495
580
|
* Choose appropriate quote character for an attribute value
|
|
496
581
|
* @param {string} attrValue - The attribute value
|
|
497
|
-
* @param {
|
|
582
|
+
* @param {{quoteCharacter?: string}} options - Minifier options
|
|
498
583
|
* @returns {string} The chosen quote character (`"` or `'`)
|
|
499
584
|
*/
|
|
500
585
|
function chooseAttributeQuote(attrValue, options) {
|
|
@@ -513,6 +598,13 @@ function chooseAttributeQuote(attrValue, options) {
|
|
|
513
598
|
|
|
514
599
|
// Returns the normalized attribute object directly (sync) or as a Promise (async);
|
|
515
600
|
// callers must handle both cases—use `isThenable()` to distinguish
|
|
601
|
+
/**
|
|
602
|
+
* @param {HTMLAttribute} attr
|
|
603
|
+
* @param {HTMLAttribute[]} attrs
|
|
604
|
+
* @param {string} tag
|
|
605
|
+
* @param {Record<string, any>} options
|
|
606
|
+
* @param {Function} minifyHTML
|
|
607
|
+
*/
|
|
516
608
|
function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
517
609
|
const attrName = options.name(attr.name);
|
|
518
610
|
let attrValue = attr.value;
|
|
@@ -528,9 +620,18 @@ function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
528
620
|
}
|
|
529
621
|
|
|
530
622
|
// Internal: Handles attribute normalization after entity decoding (if any)
|
|
623
|
+
/**
|
|
624
|
+
* @param {string} attrName
|
|
625
|
+
* @param {string | undefined} attrValue
|
|
626
|
+
* @param {HTMLAttribute} attr
|
|
627
|
+
* @param {HTMLAttribute[]} attrs
|
|
628
|
+
* @param {string} tag
|
|
629
|
+
* @param {Record<string, any>} options
|
|
630
|
+
* @param {Function} minifyHTML
|
|
631
|
+
*/
|
|
531
632
|
function normalizeAttrContinue(attrName, attrValue, attr, attrs, tag, options, minifyHTML) {
|
|
532
633
|
if ((options.removeRedundantAttributes &&
|
|
533
|
-
isAttributeRedundant(tag, attrName, attrValue, attrs)) ||
|
|
634
|
+
isAttributeRedundant(tag, attrName, attrValue ?? '', attrs)) ||
|
|
534
635
|
(options.removeScriptTypeAttributes && tag === 'script' &&
|
|
535
636
|
attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue)) ||
|
|
536
637
|
(options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
|
|
@@ -541,7 +642,7 @@ function normalizeAttrContinue(attrName, attrValue, attr, attrs, tag, options, m
|
|
|
541
642
|
if (attrValue) {
|
|
542
643
|
const cleaned = cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTML);
|
|
543
644
|
if (isThenable(cleaned)) {
|
|
544
|
-
return cleaned.then(v => normalizeAttrFinish(attrName, v, attr, tag, options));
|
|
645
|
+
return cleaned.then((/** @type {string | undefined} */ v) => normalizeAttrFinish(attrName, v, attr, tag, options));
|
|
545
646
|
}
|
|
546
647
|
return normalizeAttrFinish(attrName, cleaned, attr, tag, options);
|
|
547
648
|
}
|
|
@@ -550,6 +651,13 @@ function normalizeAttrContinue(attrName, attrValue, attr, attrs, tag, options, m
|
|
|
550
651
|
}
|
|
551
652
|
|
|
552
653
|
// Internal: Final checks and result assembly after value cleaning
|
|
654
|
+
/**
|
|
655
|
+
* @param {string} attrName
|
|
656
|
+
* @param {string | undefined} attrValue
|
|
657
|
+
* @param {HTMLAttribute} attr
|
|
658
|
+
* @param {string} tag
|
|
659
|
+
* @param {Record<string, any>} options
|
|
660
|
+
*/
|
|
553
661
|
function normalizeAttrFinish(attrName, attrValue, attr, tag, options) {
|
|
554
662
|
if (options.removeEmptyAttributes &&
|
|
555
663
|
canDeleteEmptyAttribute(tag, attrName, attrValue, options)) {
|
|
@@ -567,6 +675,13 @@ function normalizeAttrFinish(attrName, attrValue, attr, tag, options) {
|
|
|
567
675
|
};
|
|
568
676
|
}
|
|
569
677
|
|
|
678
|
+
/**
|
|
679
|
+
* @param {{name: string, value?: string, attr: HTMLAttribute}} normalized
|
|
680
|
+
* @param {string | boolean | undefined} hasUnarySlash
|
|
681
|
+
* @param {Record<string, any>} options
|
|
682
|
+
* @param {boolean} isLast
|
|
683
|
+
* @param {string | undefined} uidAttr
|
|
684
|
+
*/
|
|
570
685
|
function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
571
686
|
const attrName = normalized.name;
|
|
572
687
|
let attrValue = normalized.value;
|
|
@@ -578,7 +693,7 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
|
578
693
|
// Determine if need to add/keep quotes
|
|
579
694
|
const shouldAddQuotes = typeof attrValue !== 'undefined' && (
|
|
580
695
|
// If `removeAttributeQuotes` is enabled, add quotes only if they can’t be removed
|
|
581
|
-
(options.removeAttributeQuotes && (attrValue.indexOf(uidAttr) !== -1 || !canRemoveAttributeQuotes(attrValue))) ||
|
|
696
|
+
(options.removeAttributeQuotes && ((uidAttr ? attrValue.indexOf(uidAttr) !== -1 : false) || !canRemoveAttributeQuotes(attrValue))) ||
|
|
582
697
|
// If `removeAttributeQuotes` is not enabled, preserve original quote style or add quotes if value requires them
|
|
583
698
|
(!options.removeAttributeQuotes && (attrQuote !== '' || !canRemoveAttributeQuotes(attrValue) ||
|
|
584
699
|
// Special case: With `removeTagWhitespace`, unquoted values that aren’t last will have space added,
|
|
@@ -587,6 +702,7 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
|
587
702
|
);
|
|
588
703
|
|
|
589
704
|
if (shouldAddQuotes) {
|
|
705
|
+
attrValue = attrValue ?? '';
|
|
590
706
|
// Determine the appropriate quote character
|
|
591
707
|
if (!options.preventAttributesEscaping) {
|
|
592
708
|
// Normal mode: Choose optimal quote type to minimize escaping
|
package/src/lib/content.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// Imports
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
jsonScriptTypes
|
|
5
3
|
} from './constants.js';
|
|
@@ -9,6 +7,10 @@ import { trimWhitespace } from './whitespace.js';
|
|
|
9
7
|
|
|
10
8
|
// Wrap CSS declarations for inline styles and media queries
|
|
11
9
|
// This ensures proper context for CSS minification
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} text
|
|
12
|
+
* @param {string} type
|
|
13
|
+
*/
|
|
12
14
|
function wrapCSS(text, type) {
|
|
13
15
|
switch (type) {
|
|
14
16
|
case 'inline':
|
|
@@ -20,6 +22,10 @@ function wrapCSS(text, type) {
|
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} text
|
|
27
|
+
* @param {string} type
|
|
28
|
+
*/
|
|
23
29
|
function unwrapCSS(text, type) {
|
|
24
30
|
let matches;
|
|
25
31
|
switch (type) {
|
|
@@ -30,11 +36,15 @@ function unwrapCSS(text, type) {
|
|
|
30
36
|
matches = text.match(/^@media ([\s\S]*?)\s*{[\s\S]*}$/);
|
|
31
37
|
break;
|
|
32
38
|
}
|
|
33
|
-
return matches ? matches[1] : text;
|
|
39
|
+
return matches ? matches[1] ?? text : text;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
// Script processing
|
|
37
43
|
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} text
|
|
46
|
+
* @param {{continueOnMinifyError?: boolean, log?: Function}} options
|
|
47
|
+
*/
|
|
38
48
|
function minifyJson(text, options) {
|
|
39
49
|
try {
|
|
40
50
|
return JSON.stringify(JSON.parse(text));
|
|
@@ -48,11 +58,11 @@ function minifyJson(text, options) {
|
|
|
48
58
|
}
|
|
49
59
|
}
|
|
50
60
|
|
|
61
|
+
/** @param {Array<{name: string, value?: string}>} attrs */
|
|
51
62
|
function hasJsonScriptType(attrs) {
|
|
52
|
-
for (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const attrValue = trimWhitespace((attrs[i].value || '').split(/;/, 2)[0]).toLowerCase();
|
|
63
|
+
for (const attr of attrs) {
|
|
64
|
+
if (attr.name.toLowerCase() === 'type') {
|
|
65
|
+
const attrValue = trimWhitespace((attr.value || '').split(/;/, 2)[0] ?? '').toLowerCase();
|
|
56
66
|
if (jsonScriptTypes.has(attrValue)) {
|
|
57
67
|
return true;
|
|
58
68
|
}
|
|
@@ -61,18 +71,24 @@ function hasJsonScriptType(attrs) {
|
|
|
61
71
|
return false;
|
|
62
72
|
}
|
|
63
73
|
|
|
74
|
+
/**
|
|
75
|
+
* @param {string} text
|
|
76
|
+
* @param {{continueOnMinifyError?: boolean, log?: Function, processScripts?: string[]}} options
|
|
77
|
+
* @param {Array<{name: string, value?: string | undefined}>} currentAttrs
|
|
78
|
+
* @param {Function} minifyHTML
|
|
79
|
+
*/
|
|
64
80
|
async function processScript(text, options, currentAttrs, minifyHTML) {
|
|
65
|
-
for (
|
|
66
|
-
const attrName =
|
|
81
|
+
for (const attr of currentAttrs) {
|
|
82
|
+
const attrName = attr.name.toLowerCase();
|
|
67
83
|
if (attrName === 'type') {
|
|
68
|
-
const rawValue =
|
|
69
|
-
const normalizedValue = trimWhitespace((rawValue || '').split(/;/, 2)[0]).toLowerCase();
|
|
84
|
+
const rawValue = attr.value;
|
|
85
|
+
const normalizedValue = trimWhitespace((rawValue || '').split(/;/, 2)[0] ?? '').toLowerCase();
|
|
70
86
|
// Minify JSON script types automatically
|
|
71
87
|
if (jsonScriptTypes.has(normalizedValue)) {
|
|
72
88
|
return minifyJson(text, options);
|
|
73
89
|
}
|
|
74
90
|
// Process custom script types if specified
|
|
75
|
-
if (options.processScripts && options.processScripts.indexOf(rawValue) > -1) {
|
|
91
|
+
if (options.processScripts && rawValue && options.processScripts.indexOf(rawValue) > -1) {
|
|
76
92
|
return await minifyHTML(text, options);
|
|
77
93
|
}
|
|
78
94
|
}
|
package/src/lib/elements.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// Imports
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
headerElements,
|
|
5
3
|
descriptionElements,
|
|
@@ -13,8 +11,20 @@ import {
|
|
|
13
11
|
} from './constants.js';
|
|
14
12
|
import { hasAttrName } from './attributes.js';
|
|
15
13
|
|
|
14
|
+
/** @import { HTMLAttribute } from './attributes.js' */
|
|
15
|
+
|
|
16
|
+
// Type definitions
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {{ name: (str: string) => string, log?: Function }} MinifierOptions
|
|
20
|
+
*/
|
|
21
|
+
|
|
16
22
|
// Tag omission rules
|
|
17
23
|
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} optionalStartTag
|
|
26
|
+
* @param {string} tag
|
|
27
|
+
*/
|
|
18
28
|
function canRemoveParentTag(optionalStartTag, tag) {
|
|
19
29
|
switch (optionalStartTag) {
|
|
20
30
|
case 'html':
|
|
@@ -30,6 +40,10 @@ function canRemoveParentTag(optionalStartTag, tag) {
|
|
|
30
40
|
return false;
|
|
31
41
|
}
|
|
32
42
|
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} optionalEndTag
|
|
45
|
+
* @param {string} tag
|
|
46
|
+
*/
|
|
33
47
|
function isStartTagMandatory(optionalEndTag, tag) {
|
|
34
48
|
switch (tag) {
|
|
35
49
|
case 'colgroup':
|
|
@@ -40,6 +54,10 @@ function isStartTagMandatory(optionalEndTag, tag) {
|
|
|
40
54
|
return false;
|
|
41
55
|
}
|
|
42
56
|
|
|
57
|
+
/**
|
|
58
|
+
* @param {string} optionalEndTag
|
|
59
|
+
* @param {string} tag
|
|
60
|
+
*/
|
|
43
61
|
function canRemovePrecedingTag(optionalEndTag, tag) {
|
|
44
62
|
switch (optionalEndTag) {
|
|
45
63
|
case 'html':
|
|
@@ -79,6 +97,10 @@ function canRemovePrecedingTag(optionalEndTag, tag) {
|
|
|
79
97
|
|
|
80
98
|
// Element removal logic
|
|
81
99
|
|
|
100
|
+
/**
|
|
101
|
+
* @param {string} tag
|
|
102
|
+
* @param {HTMLAttribute[]} attrs
|
|
103
|
+
*/
|
|
82
104
|
function canRemoveElement(tag, attrs) {
|
|
83
105
|
// Elements with `id` attribute must never be removed—they serve as:
|
|
84
106
|
// - Navigation targets (skip links, URL fragments)
|
|
@@ -146,20 +168,21 @@ function parseElementSpec(str, options) {
|
|
|
146
168
|
return null;
|
|
147
169
|
}
|
|
148
170
|
|
|
149
|
-
const tag = options.name(match[1]);
|
|
150
|
-
const attrString = match[2];
|
|
171
|
+
const tag = options.name(match[1] ?? '');
|
|
172
|
+
const attrString = match[2] ?? '';
|
|
151
173
|
|
|
152
174
|
if (!attrString.trim()) {
|
|
153
175
|
return { tag, attrs: null };
|
|
154
176
|
}
|
|
155
177
|
|
|
156
178
|
// Parse attributes from string
|
|
179
|
+
/** @type {{[key: string]: string | undefined}} */
|
|
157
180
|
const attrs = {};
|
|
158
181
|
const attrRegex = /([a-zA-Z][\w:-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>/]+)))?/g;
|
|
159
182
|
let attrMatch;
|
|
160
183
|
|
|
161
184
|
while ((attrMatch = attrRegex.exec(attrString))) {
|
|
162
|
-
const attrName = options.name(attrMatch[1]);
|
|
185
|
+
const attrName = options.name(attrMatch[1] ?? '');
|
|
163
186
|
const attrValue = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4];
|
|
164
187
|
// Boolean attributes have no value (undefined)
|
|
165
188
|
attrs[attrName] = attrValue;
|
|
@@ -181,7 +204,7 @@ function parseRemoveEmptyElementsExcept(input, options) {
|
|
|
181
204
|
return [];
|
|
182
205
|
}
|
|
183
206
|
|
|
184
|
-
return input.map(item => {
|
|
207
|
+
return /** @type {Array<{tag: string, attrs: {[x: string]: string | undefined} | null}>} */ (input.map(item => {
|
|
185
208
|
if (typeof item === 'string') {
|
|
186
209
|
const spec = parseElementSpec(item, options);
|
|
187
210
|
if (!spec && options.log) {
|
|
@@ -193,7 +216,7 @@ function parseRemoveEmptyElementsExcept(input, options) {
|
|
|
193
216
|
options.log('Warning: “removeEmptyElementsExcept” specification must be a string, received: ' + typeof item);
|
|
194
217
|
}
|
|
195
218
|
return null;
|
|
196
|
-
}).filter(Boolean);
|
|
219
|
+
}).filter(Boolean));
|
|
197
220
|
}
|
|
198
221
|
|
|
199
222
|
/**
|