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/dist/htmlminifier.cjs
CHANGED
|
@@ -4,14 +4,18 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
4
4
|
|
|
5
5
|
// Stringify for options signatures (sorted keys, shallow, nested objects)
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* @param {unknown} obj
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
7
11
|
function stableStringify(obj) {
|
|
8
12
|
if (obj == null || typeof obj !== 'object') return JSON.stringify(obj);
|
|
9
13
|
if (Array.isArray(obj)) return '[' + obj.map(stableStringify).join(',') + ']';
|
|
10
14
|
const keys = Object.keys(obj).sort();
|
|
11
15
|
let out = '{';
|
|
12
16
|
for (let i = 0; i < keys.length; i++) {
|
|
13
|
-
const k = keys[i];
|
|
14
|
-
out += JSON.stringify(k) + ':' + stableStringify(obj[k]) + (i < keys.length - 1 ? ',' : '');
|
|
17
|
+
const k = keys[i] ?? '';
|
|
18
|
+
out += JSON.stringify(k) + ':' + stableStringify(/** @type {Record<string, unknown>} */ (obj)[k]) + (i < keys.length - 1 ? ',' : '');
|
|
15
19
|
}
|
|
16
20
|
return out + '}';
|
|
17
21
|
}
|
|
@@ -21,8 +25,10 @@ function stableStringify(obj) {
|
|
|
21
25
|
class LRU {
|
|
22
26
|
constructor(limit = 200) {
|
|
23
27
|
this.limit = limit;
|
|
28
|
+
/** @type {Map<string, unknown>} */
|
|
24
29
|
this.map = new Map();
|
|
25
30
|
}
|
|
31
|
+
/** @param {string} key */
|
|
26
32
|
get(key) {
|
|
27
33
|
if (this.map.has(key)) {
|
|
28
34
|
const v = this.map.get(key);
|
|
@@ -32,19 +38,25 @@ class LRU {
|
|
|
32
38
|
}
|
|
33
39
|
return undefined;
|
|
34
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} key
|
|
43
|
+
* @param {unknown} value
|
|
44
|
+
*/
|
|
35
45
|
set(key, value) {
|
|
36
46
|
if (this.map.has(key)) this.map.delete(key);
|
|
37
47
|
this.map.set(key, value);
|
|
38
48
|
if (this.map.size > this.limit) {
|
|
39
49
|
const first = this.map.keys().next().value;
|
|
40
|
-
this.map.delete(first);
|
|
50
|
+
if (first !== undefined) this.map.delete(first);
|
|
41
51
|
}
|
|
42
52
|
}
|
|
53
|
+
/** @param {string} key */
|
|
43
54
|
delete(key) { this.map.delete(key); }
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
// FNV-1a 32-bit hash for large-input cache keys
|
|
47
58
|
|
|
59
|
+
/** @param {string} str */
|
|
48
60
|
function hashContent(str) {
|
|
49
61
|
let hash = 2166136261;
|
|
50
62
|
for (let i = 0; i < str.length; i++) {
|
|
@@ -56,6 +68,7 @@ function hashContent(str) {
|
|
|
56
68
|
|
|
57
69
|
// Unique ID generator
|
|
58
70
|
|
|
71
|
+
/** @param {string} value */
|
|
59
72
|
function uniqueId(value) {
|
|
60
73
|
let id;
|
|
61
74
|
do {
|
|
@@ -66,14 +79,17 @@ function uniqueId(value) {
|
|
|
66
79
|
|
|
67
80
|
// Identity and transform functions
|
|
68
81
|
|
|
82
|
+
/** @param {string} value */
|
|
69
83
|
function identity(value) {
|
|
70
84
|
return value;
|
|
71
85
|
}
|
|
72
86
|
|
|
87
|
+
/** @param {unknown} value */
|
|
73
88
|
function isThenable(value) {
|
|
74
|
-
return value != null && typeof value === 'object' && typeof value.then === 'function';
|
|
89
|
+
return value != null && typeof value === 'object' && typeof /** @type {any} */ (value).then === 'function';
|
|
75
90
|
}
|
|
76
91
|
|
|
92
|
+
/** @param {string} value */
|
|
77
93
|
function lowercase(value) {
|
|
78
94
|
return value.toLowerCase();
|
|
79
95
|
}
|
|
@@ -88,25 +104,28 @@ function lowercase(value) {
|
|
|
88
104
|
* @returns {Promise<string>} Processed string
|
|
89
105
|
*/
|
|
90
106
|
async function replaceAsync(str, regex, asyncFn) {
|
|
107
|
+
/** @type {Promise<string>[]} */
|
|
91
108
|
const promises = [];
|
|
92
109
|
|
|
93
|
-
str.replace(regex, (match, ...args) => {
|
|
110
|
+
str.replace(regex, /** @returns {string} */ (match, ...args) => {
|
|
94
111
|
const promise = asyncFn(match, ...args);
|
|
95
112
|
promises.push(promise);
|
|
113
|
+
return match;
|
|
96
114
|
});
|
|
97
115
|
|
|
98
116
|
const data = await Promise.all(promises);
|
|
99
|
-
return str.replace(regex, () => data.shift());
|
|
117
|
+
return str.replace(regex, () => data.shift() ?? '');
|
|
100
118
|
}
|
|
101
119
|
|
|
102
120
|
// String patterns to RegExp conversion (for JSON config support)
|
|
103
121
|
|
|
122
|
+
/** @param {string | RegExp} value */
|
|
104
123
|
function parseRegExp(value) {
|
|
105
124
|
if (typeof value === 'string') {
|
|
106
125
|
if (!value) return undefined; // Empty string = not configured
|
|
107
126
|
const match = value.match(/^\/(.+)\/([dgimsuvy]*)$/);
|
|
108
127
|
if (match) {
|
|
109
|
-
return new RegExp(match[1], match[2]);
|
|
128
|
+
return new RegExp(match[1] ?? '', match[2] ?? '');
|
|
110
129
|
}
|
|
111
130
|
return new RegExp(value);
|
|
112
131
|
}
|
|
@@ -121,6 +140,25 @@ function parseRegExp(value) {
|
|
|
121
140
|
*/
|
|
122
141
|
|
|
123
142
|
|
|
143
|
+
/** @import { HTMLAttribute } from './lib/attributes.js' */
|
|
144
|
+
|
|
145
|
+
// Type definitions
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @typedef {{
|
|
149
|
+
* start?: Function,
|
|
150
|
+
* end?: Function,
|
|
151
|
+
* chars?: Function,
|
|
152
|
+
* comment?: Function,
|
|
153
|
+
* doctype?: Function,
|
|
154
|
+
* continueOnParseError?: boolean | undefined,
|
|
155
|
+
* partialMarkup?: boolean | undefined,
|
|
156
|
+
* wantsNextTag?: boolean | undefined,
|
|
157
|
+
* customAttrSurround?: RegExp[][] | undefined,
|
|
158
|
+
* customAttrAssign?: RegExp[] | undefined
|
|
159
|
+
* }} HTMLParserHandler
|
|
160
|
+
*/
|
|
161
|
+
|
|
124
162
|
/*
|
|
125
163
|
* Use like so:
|
|
126
164
|
*
|
|
@@ -133,7 +171,8 @@ function parseRegExp(value) {
|
|
|
133
171
|
*/
|
|
134
172
|
|
|
135
173
|
class CaseInsensitiveSet extends Set {
|
|
136
|
-
|
|
174
|
+
/** @override */
|
|
175
|
+
has(/** @type {string} */ str) {
|
|
137
176
|
return super.has(str.toLowerCase());
|
|
138
177
|
}
|
|
139
178
|
}
|
|
@@ -168,8 +207,9 @@ const startTagOpen = new RegExp('^<' + qnameCapture);
|
|
|
168
207
|
const endTag = new RegExp('^</' + qnameCapture + '[^>]*>');
|
|
169
208
|
|
|
170
209
|
let IS_REGEX_CAPTURING_BROKEN = false;
|
|
171
|
-
'x'.replace(/x(.)?/g, function (m, g) {
|
|
210
|
+
'x'.replace(/x(.)?/g, function (/** @type {string} */ m, /** @type {string | undefined} */ g) {
|
|
172
211
|
IS_REGEX_CAPTURING_BROKEN = g === '';
|
|
212
|
+
return m;
|
|
173
213
|
});
|
|
174
214
|
|
|
175
215
|
// Empty elements
|
|
@@ -203,6 +243,11 @@ const attrRegexCache = new WeakMap();
|
|
|
203
243
|
|
|
204
244
|
// O(n) helper: Strip all occurrences of `open…close` delimiters, keeping inner content
|
|
205
245
|
// Used instead of a regex replace to avoid O(n²) behavior on adversarial inputs
|
|
246
|
+
/**
|
|
247
|
+
* @param {string} str
|
|
248
|
+
* @param {string} open
|
|
249
|
+
* @param {string} close
|
|
250
|
+
*/
|
|
206
251
|
function stripDelimited(str, open, close) {
|
|
207
252
|
let result = '';
|
|
208
253
|
let i = 0;
|
|
@@ -218,6 +263,7 @@ function stripDelimited(str, open, close) {
|
|
|
218
263
|
return result;
|
|
219
264
|
}
|
|
220
265
|
|
|
266
|
+
/** @param {HTMLParserHandler} handler */
|
|
221
267
|
function buildAttrRegex(handler) {
|
|
222
268
|
const unquotedValue = handler.continueOnParseError ? singleAttrValueLenientUnquoted : singleAttrValues[2];
|
|
223
269
|
const attrValues = [singleAttrValues[0], singleAttrValues[1], unquotedValue];
|
|
@@ -225,12 +271,19 @@ function buildAttrRegex(handler) {
|
|
|
225
271
|
'(?:\\s*(' + joinSingleAttrAssigns(handler) + ')' +
|
|
226
272
|
'[ \\t\\n\\f\\r]*(?:' + attrValues.join('|') + '))?';
|
|
227
273
|
if (handler.customAttrSurround) {
|
|
274
|
+
if (!Array.isArray(handler.customAttrSurround)) {
|
|
275
|
+
throw new Error('`customAttrSurround` must be an array of `[RegExp, RegExp]` pairs');
|
|
276
|
+
}
|
|
228
277
|
const attrClauses = [];
|
|
229
278
|
for (let i = handler.customAttrSurround.length - 1; i >= 0; i--) {
|
|
279
|
+
const pair = handler.customAttrSurround[i];
|
|
280
|
+
if (!Array.isArray(pair) || !(pair[0] instanceof RegExp) || !(pair[1] instanceof RegExp)) {
|
|
281
|
+
throw new Error('`customAttrSurround` entries must be `[RegExp, RegExp]` pairs');
|
|
282
|
+
}
|
|
230
283
|
attrClauses[i] = '(?:' +
|
|
231
|
-
'(' +
|
|
284
|
+
'(' + pair[0].source + ')\\s*' +
|
|
232
285
|
pattern +
|
|
233
|
-
'\\s*(' +
|
|
286
|
+
'\\s*(' + pair[1].source + ')' +
|
|
234
287
|
')';
|
|
235
288
|
}
|
|
236
289
|
attrClauses.push('(?:' + pattern + ')');
|
|
@@ -239,6 +292,7 @@ function buildAttrRegex(handler) {
|
|
|
239
292
|
return new RegExp('^\\s*' + pattern);
|
|
240
293
|
}
|
|
241
294
|
|
|
295
|
+
/** @param {HTMLParserHandler} handler */
|
|
242
296
|
function getAttrRegexForHandler(handler) {
|
|
243
297
|
let cached = attrRegexCache.get(handler);
|
|
244
298
|
if (cached) return cached;
|
|
@@ -250,6 +304,7 @@ function getAttrRegexForHandler(handler) {
|
|
|
250
304
|
// Cache for sticky attribute regexes (`y` flag for position-based matching on full string)
|
|
251
305
|
const attrRegexStickyCache = new WeakMap();
|
|
252
306
|
|
|
307
|
+
/** @param {HTMLParserHandler} handler */
|
|
253
308
|
function getAttrRegexStickyForHandler(handler) {
|
|
254
309
|
let cached = attrRegexStickyCache.get(handler);
|
|
255
310
|
if (cached) return cached;
|
|
@@ -260,6 +315,7 @@ function getAttrRegexStickyForHandler(handler) {
|
|
|
260
315
|
return compiled;
|
|
261
316
|
}
|
|
262
317
|
|
|
318
|
+
/** @param {HTMLParserHandler} handler */
|
|
263
319
|
function joinSingleAttrAssigns(handler) {
|
|
264
320
|
return singleAttrAssigns.concat(
|
|
265
321
|
handler.customAttrAssign || []
|
|
@@ -272,6 +328,10 @@ function joinSingleAttrAssigns(handler) {
|
|
|
272
328
|
const NCP = 7;
|
|
273
329
|
|
|
274
330
|
class HTMLParser {
|
|
331
|
+
/**
|
|
332
|
+
* @param {string} html
|
|
333
|
+
* @param {HTMLParserHandler} handler
|
|
334
|
+
*/
|
|
275
335
|
constructor(html, handler) {
|
|
276
336
|
this.html = html;
|
|
277
337
|
this.handler = handler;
|
|
@@ -282,12 +342,21 @@ class HTMLParser {
|
|
|
282
342
|
const fullHtml = this.html;
|
|
283
343
|
const fullLength = fullHtml.length;
|
|
284
344
|
|
|
285
|
-
|
|
345
|
+
/** @type {Array<{tag: string, lowerTag: string, attrs: HTMLAttribute[]}>} */
|
|
346
|
+
const stack = [];
|
|
347
|
+
/** @type {string} */
|
|
348
|
+
let lastTag = '';
|
|
286
349
|
// Use cached attribute regex for this handler configuration
|
|
287
350
|
const attribute = getAttrRegexForHandler(handler);
|
|
288
351
|
const attributeY = getAttrRegexStickyForHandler(handler);
|
|
289
|
-
|
|
290
|
-
let
|
|
352
|
+
/** @type {string | undefined} */
|
|
353
|
+
let prevTag;
|
|
354
|
+
/** @type {string | undefined} */
|
|
355
|
+
let nextTag;
|
|
356
|
+
/** @type {HTMLAttribute[]} */
|
|
357
|
+
let prevAttrs = [];
|
|
358
|
+
/** @type {HTMLAttribute[]} */
|
|
359
|
+
let nextAttrs = [];
|
|
291
360
|
|
|
292
361
|
// Sticky regex versions for position-based matching (avoids string slicing)
|
|
293
362
|
const startTagOpenY = new RegExp(startTagOpen.source.slice(1), 'y');
|
|
@@ -307,10 +376,10 @@ class HTMLParser {
|
|
|
307
376
|
let lastPos;
|
|
308
377
|
|
|
309
378
|
// Helper to advance position
|
|
310
|
-
const advance = (n) => { pos += n; };
|
|
379
|
+
const advance = (/** @type {number} */ n) => { pos += n; };
|
|
311
380
|
|
|
312
381
|
// Lazy line/column calculation—only compute on actual errors
|
|
313
|
-
const getLineColumn = (position) => {
|
|
382
|
+
const getLineColumn = (/** @type {number} */ position) => {
|
|
314
383
|
let line = 1;
|
|
315
384
|
let column = 1;
|
|
316
385
|
for (let i = 0; i < position; i++) {
|
|
@@ -325,7 +394,7 @@ class HTMLParser {
|
|
|
325
394
|
};
|
|
326
395
|
|
|
327
396
|
// Helper to safely extract substring when needed for stacked tag content
|
|
328
|
-
const sliceFromPos = (startPos) => {
|
|
397
|
+
const sliceFromPos = (/** @type {number} */ startPos) => {
|
|
329
398
|
return fullHtml.slice(startPos);
|
|
330
399
|
};
|
|
331
400
|
|
|
@@ -353,9 +422,9 @@ class HTMLParser {
|
|
|
353
422
|
const endTagMatch = cachedNextEndTag.match;
|
|
354
423
|
cachedNextStartTag = null;
|
|
355
424
|
cachedNextEndTag = null;
|
|
356
|
-
advance(endTagMatch[0].length);
|
|
357
|
-
await parseEndTag(endTagMatch[0], endTagMatch[1]);
|
|
358
|
-
prevTag = '/' + endTagMatch[1].toLowerCase();
|
|
425
|
+
advance((endTagMatch[0] ?? '').length);
|
|
426
|
+
await parseEndTag(endTagMatch[0] ?? '', endTagMatch[1] ?? '');
|
|
427
|
+
prevTag = '/' + (endTagMatch[1] ?? '').toLowerCase();
|
|
359
428
|
prevAttrs = [];
|
|
360
429
|
continue;
|
|
361
430
|
}
|
|
@@ -413,9 +482,9 @@ class HTMLParser {
|
|
|
413
482
|
endTagY.lastIndex = pos;
|
|
414
483
|
const endTagMatch = endTagY.exec(fullHtml);
|
|
415
484
|
if (endTagMatch) {
|
|
416
|
-
advance(endTagMatch[0].length);
|
|
417
|
-
await parseEndTag(endTagMatch[0], endTagMatch[1]);
|
|
418
|
-
prevTag = '/' + endTagMatch[1].toLowerCase();
|
|
485
|
+
advance((endTagMatch[0] ?? '').length);
|
|
486
|
+
await parseEndTag(endTagMatch[0] ?? '', endTagMatch[1] ?? '');
|
|
487
|
+
prevTag = '/' + (endTagMatch[1] ?? '').toLowerCase();
|
|
419
488
|
prevAttrs = [];
|
|
420
489
|
continue;
|
|
421
490
|
}
|
|
@@ -473,12 +542,12 @@ class HTMLParser {
|
|
|
473
542
|
} else {
|
|
474
543
|
const stackedTag = lastTag.toLowerCase();
|
|
475
544
|
// Use pre-compiled regex for common tags (`script`, `style`, `noscript`) to avoid regex creation overhead
|
|
476
|
-
const reStackedTag = preCompiledStackedTags[stackedTag] || reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)\\x3c/' + stackedTag + '[^>]*>', 'i'));
|
|
545
|
+
const reStackedTag = /** @type {Record<string, RegExp>} */ (preCompiledStackedTags)[stackedTag] || /** @type {Record<string, RegExp>} */ (reCache)[stackedTag] || (/** @type {Record<string, RegExp>} */ (reCache)[stackedTag] = new RegExp('([\\s\\S]*?)\\x3c/' + stackedTag + '[^>]*>', 'i'));
|
|
477
546
|
|
|
478
547
|
const remaining = sliceFromPos(pos);
|
|
479
548
|
const m = reStackedTag.exec(remaining);
|
|
480
549
|
if (m && m.index === 0) {
|
|
481
|
-
let text = m[1];
|
|
550
|
+
let text = m[1] ?? '';
|
|
482
551
|
if (stackedTag !== 'script' && stackedTag !== 'style' && stackedTag !== 'noscript') {
|
|
483
552
|
text = stripDelimited(stripDelimited(text, '<!--', '-->'), '<![CDATA[', ']]>');
|
|
484
553
|
}
|
|
@@ -518,40 +587,39 @@ class HTMLParser {
|
|
|
518
587
|
const CONTEXT_BEFORE = 50;
|
|
519
588
|
const startPos = Math.max(0, pos - CONTEXT_BEFORE);
|
|
520
589
|
const snippet = fullHtml.slice(startPos, startPos + 200).replace(/\n/g, ' ');
|
|
521
|
-
throw new Error(
|
|
522
|
-
`Parse error at line ${loc.line}, column ${loc.column}:\n${snippet}${fullHtml.length > startPos + 200 ? '…' : ''}`
|
|
523
|
-
);
|
|
590
|
+
throw new Error(`Parse error at line ${loc.line}, column ${loc.column}:\n${snippet}${fullHtml.length > startPos + 200 ? '…' : ''}`);
|
|
524
591
|
}
|
|
525
592
|
}
|
|
526
593
|
|
|
527
594
|
if (!handler.partialMarkup) {
|
|
528
595
|
// Clean up any remaining tags
|
|
529
|
-
await parseEndTag();
|
|
596
|
+
await parseEndTag('', '');
|
|
530
597
|
}
|
|
531
598
|
|
|
532
599
|
// Helper to extract minimal attribute info (name/value pairs) from raw attribute matches
|
|
533
600
|
// Used for whitespace collapsing logic—doesn’t need full processing
|
|
534
|
-
function extractAttrInfo(rawAttrs) {
|
|
601
|
+
function extractAttrInfo(/** @type {Array<Array<string | undefined>>} */ rawAttrs) {
|
|
535
602
|
if (!rawAttrs || !rawAttrs.length) return [];
|
|
536
603
|
|
|
537
604
|
const numCustomParts = handler.customAttrSurround ? handler.customAttrSurround.length * NCP : 0;
|
|
538
605
|
const baseIndex = 1 + numCustomParts;
|
|
539
606
|
|
|
540
|
-
return rawAttrs.map(args => {
|
|
607
|
+
return /** @type {HTMLAttribute[]} */ (rawAttrs.map((/** @type {Array<string | undefined>} */ args) => {
|
|
541
608
|
// Extract attribute name (always at `baseIndex`)
|
|
542
609
|
const name = args[baseIndex];
|
|
543
610
|
// Extract value from double-quoted (`baseIndex + 2`), single-quoted (`baseIndex + 3`), or unquoted (`baseIndex + 4`)
|
|
544
611
|
const value = args[baseIndex + 2] ?? args[baseIndex + 3] ?? args[baseIndex + 4];
|
|
545
|
-
return { name: name?.toLowerCase(), value };
|
|
546
|
-
}).filter(attr => attr.name); // Filter out invalid entries
|
|
612
|
+
return { name: name?.toLowerCase() ?? '', value };
|
|
613
|
+
}).filter(attr => attr.name)); // Filter out invalid entries
|
|
547
614
|
}
|
|
548
615
|
|
|
549
|
-
function parseStartTag(startPos) {
|
|
616
|
+
function parseStartTag(/** @type {number} */ startPos) {
|
|
550
617
|
startTagOpenY.lastIndex = startPos;
|
|
551
618
|
const start = startTagOpenY.exec(fullHtml);
|
|
552
619
|
if (start) {
|
|
620
|
+
/** @type {{tagName: string, attrs: Array<Array<string|undefined>>, advance: number, unarySlash?: string}} */
|
|
553
621
|
const match = {
|
|
554
|
-
tagName: start[1],
|
|
622
|
+
tagName: start[1] ?? '',
|
|
555
623
|
attrs: [],
|
|
556
624
|
advance: 0
|
|
557
625
|
};
|
|
@@ -694,19 +762,21 @@ class HTMLParser {
|
|
|
694
762
|
startTagCloseY.lastIndex = currentPos;
|
|
695
763
|
end = startTagCloseY.exec(fullHtml);
|
|
696
764
|
if (end) {
|
|
697
|
-
match.unarySlash = end[1];
|
|
698
|
-
consumed += end[0].length;
|
|
765
|
+
match.unarySlash = end[1] ?? '';
|
|
766
|
+
consumed += (end[0] ?? '').length;
|
|
699
767
|
match.advance = consumed;
|
|
700
768
|
return match;
|
|
701
769
|
}
|
|
702
770
|
}
|
|
771
|
+
return undefined;
|
|
703
772
|
}
|
|
704
773
|
|
|
705
|
-
function findTagInCurrentTable(tagName) {
|
|
774
|
+
function findTagInCurrentTable(/** @type {string} */ tagName) {
|
|
706
775
|
let pos;
|
|
707
776
|
const needle = tagName.toLowerCase();
|
|
708
777
|
for (pos = stack.length - 1; pos >= 0; pos--) {
|
|
709
|
-
const
|
|
778
|
+
const entry = stack[pos];
|
|
779
|
+
const currentTag = entry?.lowerTag;
|
|
710
780
|
if (currentTag === needle) {
|
|
711
781
|
return pos;
|
|
712
782
|
}
|
|
@@ -718,18 +788,19 @@ class HTMLParser {
|
|
|
718
788
|
return -1;
|
|
719
789
|
}
|
|
720
790
|
|
|
721
|
-
async function parseEndTagAt(pos) {
|
|
791
|
+
async function parseEndTagAt(/** @type {number} */ pos) {
|
|
722
792
|
// Close all open elements up to `pos` (mirrors `parseEndTag`’s core branch)
|
|
723
793
|
for (let i = stack.length - 1; i >= pos; i--) {
|
|
794
|
+
const entry = /** @type {NonNullable<(typeof stack)[number]>} */ (stack[i]);
|
|
724
795
|
if (handler.end) {
|
|
725
|
-
await handler.end(
|
|
796
|
+
await handler.end(entry.tag, entry.attrs, true);
|
|
726
797
|
}
|
|
727
798
|
}
|
|
728
799
|
stack.length = pos;
|
|
729
|
-
lastTag = pos
|
|
800
|
+
lastTag = pos ? (stack[pos - 1]?.tag ?? '') : '';
|
|
730
801
|
}
|
|
731
802
|
|
|
732
|
-
async function closeIfFoundInCurrentTable(tagName) {
|
|
803
|
+
async function closeIfFoundInCurrentTable(/** @type {string} */ tagName) {
|
|
733
804
|
const pos = findTagInCurrentTable(tagName);
|
|
734
805
|
if (pos >= 0) {
|
|
735
806
|
// Close at the specific index to avoid re-searching
|
|
@@ -739,7 +810,7 @@ class HTMLParser {
|
|
|
739
810
|
return false;
|
|
740
811
|
}
|
|
741
812
|
|
|
742
|
-
async function handleStartTag(match) {
|
|
813
|
+
async function handleStartTag(/** @type {{tagName: string, attrs: Array<Array<string | undefined>>, advance: number, unarySlash?: string}} */ match) {
|
|
743
814
|
const tagName = match.tagName;
|
|
744
815
|
let unarySlash = match.unarySlash;
|
|
745
816
|
|
|
@@ -781,17 +852,18 @@ class HTMLParser {
|
|
|
781
852
|
|
|
782
853
|
const unary = empty.has(tagName) || (tagName === 'html' && lastTag === 'head') || !!unarySlash;
|
|
783
854
|
|
|
784
|
-
const attrs = match.attrs.map(function (args) {
|
|
855
|
+
const attrs = /** @type {HTMLAttribute[]} */ (match.attrs.map(function (/** @type {Array<string | undefined>} */ args) {
|
|
856
|
+
/** @type {string | undefined} */
|
|
785
857
|
let name, value, customOpen, customClose, customAssign, quote;
|
|
786
858
|
|
|
787
859
|
// Hackish workaround for Firefox bug, https://bugzilla.mozilla.org/show_bug.cgi?id=369778
|
|
788
|
-
if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
|
|
860
|
+
if (IS_REGEX_CAPTURING_BROKEN && (args[0] ?? '').indexOf('""') === -1) {
|
|
789
861
|
if (args[3] === '') { delete args[3]; }
|
|
790
862
|
if (args[4] === '') { delete args[4]; }
|
|
791
863
|
if (args[5] === '') { delete args[5]; }
|
|
792
864
|
}
|
|
793
865
|
|
|
794
|
-
function populate(index) {
|
|
866
|
+
function populate(/** @type {number} */ index) {
|
|
795
867
|
customAssign = args[index];
|
|
796
868
|
value = args[index + 1];
|
|
797
869
|
if (typeof value !== 'undefined') {
|
|
@@ -802,7 +874,7 @@ class HTMLParser {
|
|
|
802
874
|
return '\'';
|
|
803
875
|
}
|
|
804
876
|
value = args[index + 3];
|
|
805
|
-
if (typeof value === 'undefined' && fillAttrs.has(name)) {
|
|
877
|
+
if (typeof value === 'undefined' && name && fillAttrs.has(name)) {
|
|
806
878
|
value = name;
|
|
807
879
|
}
|
|
808
880
|
return '';
|
|
@@ -826,14 +898,14 @@ class HTMLParser {
|
|
|
826
898
|
}
|
|
827
899
|
|
|
828
900
|
return {
|
|
829
|
-
name,
|
|
901
|
+
name: name ?? '',
|
|
830
902
|
value,
|
|
831
903
|
customAssign: customAssign || '=',
|
|
832
904
|
customOpen: customOpen || '',
|
|
833
905
|
customClose: customClose || '',
|
|
834
906
|
quote: quote || ''
|
|
835
907
|
};
|
|
836
|
-
});
|
|
908
|
+
}));
|
|
837
909
|
|
|
838
910
|
if (!unary) {
|
|
839
911
|
stack.push({ tag: tagName, lowerTag: tagName.toLowerCase(), attrs });
|
|
@@ -849,18 +921,18 @@ class HTMLParser {
|
|
|
849
921
|
}
|
|
850
922
|
}
|
|
851
923
|
|
|
852
|
-
function findTag(tagName) {
|
|
924
|
+
function findTag(/** @type {string} */ tagName) {
|
|
853
925
|
let pos;
|
|
854
926
|
const needle = tagName.toLowerCase();
|
|
855
927
|
for (pos = stack.length - 1; pos >= 0; pos--) {
|
|
856
|
-
if (stack[pos]
|
|
928
|
+
if (stack[pos]?.lowerTag === needle) {
|
|
857
929
|
break;
|
|
858
930
|
}
|
|
859
931
|
}
|
|
860
932
|
return pos;
|
|
861
933
|
}
|
|
862
934
|
|
|
863
|
-
async function parseEndTag(tag, tagName) {
|
|
935
|
+
async function parseEndTag(/** @type {string} */ tag, /** @type {string} */ tagName) {
|
|
864
936
|
let pos;
|
|
865
937
|
|
|
866
938
|
// Find the closest opened tag of the same type
|
|
@@ -874,13 +946,13 @@ class HTMLParser {
|
|
|
874
946
|
// Close all the open elements, up the stack
|
|
875
947
|
for (let i = stack.length - 1; i >= pos; i--) {
|
|
876
948
|
if (handler.end) {
|
|
877
|
-
handler.end(stack[i]
|
|
949
|
+
handler.end(stack[i]?.tag, stack[i]?.attrs, i > pos || !tag);
|
|
878
950
|
}
|
|
879
951
|
}
|
|
880
952
|
|
|
881
953
|
// Remove the open elements from the stack
|
|
882
954
|
stack.length = pos;
|
|
883
|
-
lastTag = pos
|
|
955
|
+
lastTag = pos ? (stack[pos - 1]?.tag ?? '') : '';
|
|
884
956
|
} else if (handler.partialMarkup && tagName) {
|
|
885
957
|
// In partial markup mode, preserve stray end tags
|
|
886
958
|
if (handler.end) {
|
|
@@ -903,19 +975,31 @@ class HTMLParser {
|
|
|
903
975
|
}
|
|
904
976
|
|
|
905
977
|
class Sorter {
|
|
978
|
+
constructor() {
|
|
979
|
+
/** @type {string[]} */
|
|
980
|
+
this.keys = [];
|
|
981
|
+
/** @type {Map<string, Sorter>} */
|
|
982
|
+
this.sorterMap = new Map();
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* @param {string[]} tokens
|
|
987
|
+
* @param {number} fromIndex
|
|
988
|
+
* @returns {string[]}
|
|
989
|
+
*/
|
|
906
990
|
sort(tokens, fromIndex = 0) {
|
|
907
|
-
for (
|
|
908
|
-
const token = this.keys[i];
|
|
991
|
+
for (const token of this.keys) {
|
|
909
992
|
|
|
910
993
|
// Single pass: Count matches and collect non-matches
|
|
911
994
|
let matchCount = 0;
|
|
912
995
|
const others = [];
|
|
913
996
|
|
|
914
997
|
for (let j = fromIndex; j < tokens.length; j++) {
|
|
915
|
-
|
|
998
|
+
const t = /** @type {string} */ (tokens[j]);
|
|
999
|
+
if (t === token) {
|
|
916
1000
|
matchCount++;
|
|
917
1001
|
} else {
|
|
918
|
-
others.push(
|
|
1002
|
+
others.push(t);
|
|
919
1003
|
}
|
|
920
1004
|
}
|
|
921
1005
|
|
|
@@ -925,12 +1009,12 @@ class Sorter {
|
|
|
925
1009
|
for (let j = 0; j < matchCount; j++) {
|
|
926
1010
|
tokens[writeIdx++] = token;
|
|
927
1011
|
}
|
|
928
|
-
for (
|
|
929
|
-
tokens[writeIdx++] =
|
|
1012
|
+
for (const other of others) {
|
|
1013
|
+
tokens[writeIdx++] = other;
|
|
930
1014
|
}
|
|
931
1015
|
|
|
932
1016
|
const newFromIndex = fromIndex + matchCount;
|
|
933
|
-
return this.sorterMap.get(token)
|
|
1017
|
+
return this.sorterMap.get(token)?.sort(tokens, newFromIndex) ?? tokens;
|
|
934
1018
|
}
|
|
935
1019
|
}
|
|
936
1020
|
return tokens;
|
|
@@ -939,22 +1023,24 @@ class Sorter {
|
|
|
939
1023
|
|
|
940
1024
|
class TokenChain {
|
|
941
1025
|
constructor() {
|
|
942
|
-
|
|
1026
|
+
/** @type {Map<string, {arrays: string[][], processed: number}>} */
|
|
943
1027
|
this.map = new Map();
|
|
944
1028
|
}
|
|
945
1029
|
|
|
1030
|
+
/** @param {string[]} tokens */
|
|
946
1031
|
add(tokens) {
|
|
947
1032
|
tokens.forEach((token) => {
|
|
948
|
-
|
|
949
|
-
|
|
1033
|
+
let entry = this.map.get(token);
|
|
1034
|
+
if (!entry) {
|
|
1035
|
+
entry = { arrays: [], processed: 0 };
|
|
1036
|
+
this.map.set(token, entry);
|
|
950
1037
|
}
|
|
951
|
-
|
|
1038
|
+
entry.arrays.push(tokens);
|
|
952
1039
|
});
|
|
953
1040
|
}
|
|
954
1041
|
|
|
955
1042
|
createSorter() {
|
|
956
1043
|
const sorter = new Sorter();
|
|
957
|
-
sorter.sorterMap = new Map();
|
|
958
1044
|
|
|
959
1045
|
// Convert map entries to array and sort by frequency (descending), then alphabetically
|
|
960
1046
|
const entries = Array.from(this.map.entries()).sort((a, b) => {
|
|
@@ -967,18 +1053,17 @@ class TokenChain {
|
|
|
967
1053
|
return a[0].localeCompare(b[0]);
|
|
968
1054
|
});
|
|
969
1055
|
|
|
970
|
-
sorter.keys = [];
|
|
971
|
-
|
|
972
1056
|
entries.forEach(([token, data]) => {
|
|
973
1057
|
if (data.processed < data.arrays.length) {
|
|
974
1058
|
const chain = new TokenChain();
|
|
975
1059
|
|
|
976
1060
|
data.arrays.forEach((tokens) => {
|
|
977
1061
|
// Build new array without the current token instead of splicing
|
|
1062
|
+
/** @type {string[]} */
|
|
978
1063
|
const filtered = [];
|
|
979
|
-
for (
|
|
980
|
-
if (
|
|
981
|
-
filtered.push(
|
|
1064
|
+
for (const t of tokens) {
|
|
1065
|
+
if (t !== token) {
|
|
1066
|
+
filtered.push(t);
|
|
982
1067
|
}
|
|
983
1068
|
}
|
|
984
1069
|
|
|
@@ -1053,7 +1138,7 @@ const presets = {
|
|
|
1053
1138
|
function getPreset(name) {
|
|
1054
1139
|
if (!name) return null;
|
|
1055
1140
|
const normalizedName = name.toLowerCase();
|
|
1056
|
-
return presets[normalizedName] || null;
|
|
1141
|
+
return /** @type {Record<string, object>} */ (presets)[normalizedName] || null;
|
|
1057
1142
|
}
|
|
1058
1143
|
|
|
1059
1144
|
/**
|
|
@@ -1238,11 +1323,9 @@ const reEmptyAttribute = new RegExp(
|
|
|
1238
1323
|
|
|
1239
1324
|
const specialContentElements = new Set(['script', 'style']);
|
|
1240
1325
|
|
|
1241
|
-
// Imports
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
1326
|
// Trim whitespace
|
|
1245
1327
|
|
|
1328
|
+
/** @param {string} str */
|
|
1246
1329
|
const trimWhitespace = str => {
|
|
1247
1330
|
if (!str) return str;
|
|
1248
1331
|
// Fast path: If no whitespace at start or end, return early
|
|
@@ -1254,6 +1337,7 @@ const trimWhitespace = str => {
|
|
|
1254
1337
|
|
|
1255
1338
|
// Collapse all whitespace
|
|
1256
1339
|
|
|
1340
|
+
/** @param {string} str */
|
|
1257
1341
|
function collapseWhitespaceAll(str) {
|
|
1258
1342
|
if (!str) return str;
|
|
1259
1343
|
// Fast path: If there are no common whitespace characters, return early
|
|
@@ -1261,7 +1345,7 @@ function collapseWhitespaceAll(str) {
|
|
|
1261
1345
|
return str;
|
|
1262
1346
|
}
|
|
1263
1347
|
// No-break space is specifically handled inside the replacer function here:
|
|
1264
|
-
return str.replace(RE_ALL_WS_NBSP, function (spaces) {
|
|
1348
|
+
return str.replace(RE_ALL_WS_NBSP, function (/** @param {string} spaces */ spaces) {
|
|
1265
1349
|
// Preserve standalone tabs
|
|
1266
1350
|
if (spaces === '\t') return '\t';
|
|
1267
1351
|
// Fast path: No no-break space, common case—just collapse to single space
|
|
@@ -1274,7 +1358,14 @@ function collapseWhitespaceAll(str) {
|
|
|
1274
1358
|
|
|
1275
1359
|
// Collapse whitespace with options
|
|
1276
1360
|
|
|
1277
|
-
|
|
1361
|
+
/**
|
|
1362
|
+
* @param {string} str
|
|
1363
|
+
* @param {{preserveLineBreaks?: boolean | undefined, conservativeCollapse?: boolean | undefined}} options
|
|
1364
|
+
* @param {boolean} trimLeft
|
|
1365
|
+
* @param {boolean} trimRight
|
|
1366
|
+
* @param {boolean} [collapseAll]
|
|
1367
|
+
*/
|
|
1368
|
+
function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll = false) {
|
|
1278
1369
|
let lineBreakBefore = ''; let lineBreakAfter = '';
|
|
1279
1370
|
|
|
1280
1371
|
if (!str) return str;
|
|
@@ -1294,7 +1385,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
1294
1385
|
// (avoids polynomial backtracking with end-anchored lazy quantifiers)
|
|
1295
1386
|
const WS_CHARS = ' \n\r\t\f';
|
|
1296
1387
|
let leadEnd = 0;
|
|
1297
|
-
while (leadEnd < str.length && WS_CHARS.includes(str
|
|
1388
|
+
while (leadEnd < str.length && WS_CHARS.includes(str.charAt(leadEnd))) {
|
|
1298
1389
|
leadEnd++;
|
|
1299
1390
|
}
|
|
1300
1391
|
if (leadEnd > 0) {
|
|
@@ -1305,7 +1396,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
1305
1396
|
}
|
|
1306
1397
|
}
|
|
1307
1398
|
let trailStart = str.length;
|
|
1308
|
-
while (trailStart > 0 && WS_CHARS.includes(str
|
|
1399
|
+
while (trailStart > 0 && WS_CHARS.includes(str.charAt(trailStart - 1))) {
|
|
1309
1400
|
trailStart--;
|
|
1310
1401
|
}
|
|
1311
1402
|
if (trailStart < str.length) {
|
|
@@ -1319,7 +1410,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
1319
1410
|
|
|
1320
1411
|
if (trimLeft) {
|
|
1321
1412
|
// No-break space is specifically handled inside the replacer function
|
|
1322
|
-
str = str.replace(/^[ \n\r\t\f\xA0]+/, function (spaces) {
|
|
1413
|
+
str = str.replace(/^[ \n\r\t\f\xA0]+/, function (/** @type {string} */ spaces) {
|
|
1323
1414
|
const conservative = !lineBreakBefore && options.conservativeCollapse;
|
|
1324
1415
|
if (conservative && spaces === '\t') {
|
|
1325
1416
|
return '\t';
|
|
@@ -1332,7 +1423,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
1332
1423
|
// Find trailing whitespace boundary manually (avoids polynomial backtracking
|
|
1333
1424
|
// with `/[ \n\r\t\f\xA0]+$/` on strings with long internal whitespace runs)
|
|
1334
1425
|
let end = str.length;
|
|
1335
|
-
while (end > 0 && ' \n\r\t\f\xA0'.includes(str
|
|
1426
|
+
while (end > 0 && ' \n\r\t\f\xA0'.includes(str.charAt(end - 1))) {
|
|
1336
1427
|
end--;
|
|
1337
1428
|
}
|
|
1338
1429
|
if (end < str.length) {
|
|
@@ -1363,14 +1454,24 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
1363
1454
|
|
|
1364
1455
|
// Collapse whitespace smartly based on surrounding tags
|
|
1365
1456
|
|
|
1457
|
+
/**
|
|
1458
|
+
* @param {string} str
|
|
1459
|
+
* @param {string} prevTag
|
|
1460
|
+
* @param {string} nextTag
|
|
1461
|
+
* @param {Array<{name: string, value?: string}>} prevAttrs
|
|
1462
|
+
* @param {Array<{name: string, value?: string}>} nextAttrs
|
|
1463
|
+
* @param {{preserveLineBreaks?: boolean, conservativeCollapse?: boolean, collapseInlineTagWhitespace?: boolean}} options
|
|
1464
|
+
* @param {Set<string>} inlineElements
|
|
1465
|
+
* @param {Set<string>} inlineTextSet
|
|
1466
|
+
*/
|
|
1366
1467
|
function collapseWhitespaceSmart(str, prevTag, nextTag, prevAttrs, nextAttrs, options, inlineElements, inlineTextSet) {
|
|
1367
1468
|
const prevTagName = prevTag && (prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag);
|
|
1368
1469
|
const nextTagName = nextTag && (nextTag.charAt(0) === '/' ? nextTag.slice(1) : nextTag);
|
|
1369
1470
|
|
|
1370
1471
|
// Helper: Check if an input element has `type="hidden"`
|
|
1371
|
-
const isHiddenInput = (tagName, attrs) => {
|
|
1472
|
+
const isHiddenInput = (/** @type {string} */ tagName, /** @type {Array<{name: string, value?: string}>} */ attrs) => {
|
|
1372
1473
|
if (tagName !== 'input' || !attrs || !attrs.length) return false;
|
|
1373
|
-
const typeAttr = attrs.find(attr => attr.name === 'type');
|
|
1474
|
+
const typeAttr = attrs.find((/** @type {{name: string, value?: string}} */ attr) => attr.name === 'type');
|
|
1374
1475
|
return typeAttr && typeAttr.value === 'hidden';
|
|
1375
1476
|
};
|
|
1376
1477
|
|
|
@@ -1434,7 +1535,7 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, prevAttrs, nextAttrs, op
|
|
|
1434
1535
|
}
|
|
1435
1536
|
}
|
|
1436
1537
|
|
|
1437
|
-
return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
|
|
1538
|
+
return collapseWhitespace(str, options, Boolean(trimLeft), Boolean(trimRight), Boolean(prevTag && nextTag));
|
|
1438
1539
|
}
|
|
1439
1540
|
|
|
1440
1541
|
// Collapse/trim whitespace for given tag
|
|
@@ -1442,10 +1543,12 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, prevAttrs, nextAttrs, op
|
|
|
1442
1543
|
const noCollapseWhitespaceTags = new Set(['script', 'style', 'pre', 'textarea']);
|
|
1443
1544
|
const noTrimWhitespaceTags = new Set(['pre', 'textarea']);
|
|
1444
1545
|
|
|
1546
|
+
/** @param {string} tag */
|
|
1445
1547
|
function canCollapseWhitespace(tag) {
|
|
1446
1548
|
return !noCollapseWhitespaceTags.has(tag);
|
|
1447
1549
|
}
|
|
1448
1550
|
|
|
1551
|
+
/** @param {string} tag */
|
|
1449
1552
|
function canTrimWhitespace(tag) {
|
|
1450
1553
|
return !noTrimWhitespaceTags.has(tag);
|
|
1451
1554
|
}
|
|
@@ -1606,13 +1709,14 @@ function createUrlMinifier(site) {
|
|
|
1606
1709
|
};
|
|
1607
1710
|
}
|
|
1608
1711
|
|
|
1609
|
-
// Imports
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
1712
|
// CSS processing
|
|
1613
1713
|
|
|
1614
1714
|
// Wrap CSS declarations for inline styles and media queries
|
|
1615
1715
|
// This ensures proper context for CSS minification
|
|
1716
|
+
/**
|
|
1717
|
+
* @param {string} text
|
|
1718
|
+
* @param {string} type
|
|
1719
|
+
*/
|
|
1616
1720
|
function wrapCSS(text, type) {
|
|
1617
1721
|
switch (type) {
|
|
1618
1722
|
case 'inline':
|
|
@@ -1624,6 +1728,10 @@ function wrapCSS(text, type) {
|
|
|
1624
1728
|
}
|
|
1625
1729
|
}
|
|
1626
1730
|
|
|
1731
|
+
/**
|
|
1732
|
+
* @param {string} text
|
|
1733
|
+
* @param {string} type
|
|
1734
|
+
*/
|
|
1627
1735
|
function unwrapCSS(text, type) {
|
|
1628
1736
|
let matches;
|
|
1629
1737
|
switch (type) {
|
|
@@ -1634,11 +1742,15 @@ function unwrapCSS(text, type) {
|
|
|
1634
1742
|
matches = text.match(/^@media ([\s\S]*?)\s*{[\s\S]*}$/);
|
|
1635
1743
|
break;
|
|
1636
1744
|
}
|
|
1637
|
-
return matches ? matches[1] : text;
|
|
1745
|
+
return matches ? matches[1] ?? text : text;
|
|
1638
1746
|
}
|
|
1639
1747
|
|
|
1640
1748
|
// Script processing
|
|
1641
1749
|
|
|
1750
|
+
/**
|
|
1751
|
+
* @param {string} text
|
|
1752
|
+
* @param {{continueOnMinifyError?: boolean, log?: Function}} options
|
|
1753
|
+
*/
|
|
1642
1754
|
function minifyJson(text, options) {
|
|
1643
1755
|
try {
|
|
1644
1756
|
return JSON.stringify(JSON.parse(text));
|
|
@@ -1652,11 +1764,11 @@ function minifyJson(text, options) {
|
|
|
1652
1764
|
}
|
|
1653
1765
|
}
|
|
1654
1766
|
|
|
1767
|
+
/** @param {Array<{name: string, value?: string}>} attrs */
|
|
1655
1768
|
function hasJsonScriptType(attrs) {
|
|
1656
|
-
for (
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
const attrValue = trimWhitespace((attrs[i].value || '').split(/;/, 2)[0]).toLowerCase();
|
|
1769
|
+
for (const attr of attrs) {
|
|
1770
|
+
if (attr.name.toLowerCase() === 'type') {
|
|
1771
|
+
const attrValue = trimWhitespace((attr.value || '').split(/;/, 2)[0] ?? '').toLowerCase();
|
|
1660
1772
|
if (jsonScriptTypes.has(attrValue)) {
|
|
1661
1773
|
return true;
|
|
1662
1774
|
}
|
|
@@ -1665,18 +1777,24 @@ function hasJsonScriptType(attrs) {
|
|
|
1665
1777
|
return false;
|
|
1666
1778
|
}
|
|
1667
1779
|
|
|
1780
|
+
/**
|
|
1781
|
+
* @param {string} text
|
|
1782
|
+
* @param {{continueOnMinifyError?: boolean, log?: Function, processScripts?: string[]}} options
|
|
1783
|
+
* @param {Array<{name: string, value?: string | undefined}>} currentAttrs
|
|
1784
|
+
* @param {Function} minifyHTML
|
|
1785
|
+
*/
|
|
1668
1786
|
async function processScript(text, options, currentAttrs, minifyHTML) {
|
|
1669
|
-
for (
|
|
1670
|
-
const attrName =
|
|
1787
|
+
for (const attr of currentAttrs) {
|
|
1788
|
+
const attrName = attr.name.toLowerCase();
|
|
1671
1789
|
if (attrName === 'type') {
|
|
1672
|
-
const rawValue =
|
|
1673
|
-
const normalizedValue = trimWhitespace((rawValue || '').split(/;/, 2)[0]).toLowerCase();
|
|
1790
|
+
const rawValue = attr.value;
|
|
1791
|
+
const normalizedValue = trimWhitespace((rawValue || '').split(/;/, 2)[0] ?? '').toLowerCase();
|
|
1674
1792
|
// Minify JSON script types automatically
|
|
1675
1793
|
if (jsonScriptTypes.has(normalizedValue)) {
|
|
1676
1794
|
return minifyJson(text, options);
|
|
1677
1795
|
}
|
|
1678
1796
|
// Process custom script types if specified
|
|
1679
|
-
if (options.processScripts && options.processScripts.indexOf(rawValue) > -1) {
|
|
1797
|
+
if (options.processScripts && rawValue && options.processScripts.indexOf(rawValue) > -1) {
|
|
1680
1798
|
return await minifyHTML(text, options);
|
|
1681
1799
|
}
|
|
1682
1800
|
}
|
|
@@ -1700,11 +1818,17 @@ const optionDefaults = {
|
|
|
1700
1818
|
includeAutoGeneratedTags: false
|
|
1701
1819
|
};
|
|
1702
1820
|
|
|
1703
|
-
//
|
|
1821
|
+
// Type definitions
|
|
1822
|
+
|
|
1823
|
+
/**
|
|
1824
|
+
* @typedef {Record<string, any>} MinifierOptions
|
|
1825
|
+
*/
|
|
1704
1826
|
|
|
1827
|
+
// @@ Extract a `ProcessedOptions` typedef (with `name`, `log`, `canCollapseWhitespace`, `canTrimWhitespace`,`minifyCSS`, `minifyJS`, `minifyURLs` typed as required, non-optional) to replace the current `Record<string, any>` escape hatch and eliminate the `/** @type {Function} */` casts in htmlminifier.js
|
|
1705
1828
|
|
|
1706
1829
|
// Helper functions
|
|
1707
1830
|
|
|
1831
|
+
/** @param {MinifierOptions} options */
|
|
1708
1832
|
function shouldMinifyInnerHTML(options) {
|
|
1709
1833
|
return Boolean(
|
|
1710
1834
|
options.collapseWhitespace ||
|
|
@@ -1720,18 +1844,12 @@ function shouldMinifyInnerHTML(options) {
|
|
|
1720
1844
|
// Main options processor
|
|
1721
1845
|
|
|
1722
1846
|
/**
|
|
1723
|
-
* @param {
|
|
1724
|
-
* @param {
|
|
1725
|
-
* @param {Function} deps.getLightningCSS - Function to lazily load Lightning CSS
|
|
1726
|
-
* @param {Function} deps.getTerser - Function to lazily load Terser
|
|
1727
|
-
* @param {Function} deps.getSwc - Function to lazily load @swc/core
|
|
1728
|
-
* @param {Function} deps.getSvgo - Function to lazily load SVGO
|
|
1729
|
-
* @param {LRU} deps.cssMinifyCache - CSS minification cache
|
|
1730
|
-
* @param {LRU} deps.jsMinifyCache - JS minification cache
|
|
1731
|
-
* @param {LRU} deps.svgMinifyCache - SVG minification cache
|
|
1847
|
+
* @param {MinifierOptions} inputOptions - User-provided options
|
|
1848
|
+
* @param {{getLightningCSS?: Function | undefined, getTerser?: Function | undefined, getSwc?: Function | undefined, getSvgo?: Function | undefined, cssMinifyCache?: LRU | undefined, jsMinifyCache?: LRU | undefined, svgMinifyCache?: LRU | undefined}} [deps] - Dependencies from htmlminifier.js
|
|
1732
1849
|
* @returns {MinifierOptions} Normalized options with defaults applied
|
|
1733
1850
|
*/
|
|
1734
1851
|
const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getSvgo, cssMinifyCache, jsMinifyCache, svgMinifyCache } = {}) => {
|
|
1852
|
+
/** @type {MinifierOptions} */
|
|
1735
1853
|
const options = {
|
|
1736
1854
|
name: lowercase,
|
|
1737
1855
|
canCollapseWhitespace,
|
|
@@ -1744,13 +1862,13 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
1744
1862
|
minifySVG: null
|
|
1745
1863
|
};
|
|
1746
1864
|
|
|
1747
|
-
const parseRegExpArray = (arr) => {
|
|
1748
|
-
return Array.isArray(arr) ? arr.map(parseRegExp) :
|
|
1865
|
+
const parseRegExpArray = (/** @type {unknown} */ arr) => {
|
|
1866
|
+
return Array.isArray(arr) ? arr.map(parseRegExp) : [];
|
|
1749
1867
|
};
|
|
1750
1868
|
|
|
1751
1869
|
// Helper for nested arrays (e.g., `customAttrSurround: [[start, end], …]`)
|
|
1752
|
-
const parseNestedRegExpArray = (arr) => {
|
|
1753
|
-
if (!Array.isArray(arr)) return
|
|
1870
|
+
const parseNestedRegExpArray = (/** @type {unknown} */ arr) => {
|
|
1871
|
+
if (!Array.isArray(arr)) return [];
|
|
1754
1872
|
return arr.map(item => {
|
|
1755
1873
|
// If item is an array (a pair), recursively convert each element
|
|
1756
1874
|
if (Array.isArray(item)) {
|
|
@@ -1791,13 +1909,16 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
1791
1909
|
options.log = option;
|
|
1792
1910
|
}
|
|
1793
1911
|
} else if (key === 'minifyCSS' && typeof option !== 'function') {
|
|
1794
|
-
if (!option) {
|
|
1912
|
+
if (!option || !getLightningCSS || !cssMinifyCache) {
|
|
1795
1913
|
return;
|
|
1796
1914
|
}
|
|
1797
1915
|
|
|
1798
1916
|
const lightningCssOptions = typeof option === 'object' ? option : {};
|
|
1917
|
+
// Capture to preserve TypeScript narrowing across the async closure boundary below
|
|
1918
|
+
const cssLoader = getLightningCSS;
|
|
1919
|
+
const cssCache = cssMinifyCache;
|
|
1799
1920
|
|
|
1800
|
-
options.minifyCSS = async function (text, type) {
|
|
1921
|
+
options.minifyCSS = async function (/** @type {string} */ text, /** @type {string} */ type) {
|
|
1801
1922
|
// Fast path: Nothing to minify
|
|
1802
1923
|
if (!text || !text.trim()) {
|
|
1803
1924
|
return text;
|
|
@@ -1809,7 +1930,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
1809
1930
|
text = await replaceAsync(
|
|
1810
1931
|
text,
|
|
1811
1932
|
/(url\s*\(\s*)(?:"([^"]*)"|'([^']*)'|([^\s)]+))(\s*\))/ig,
|
|
1812
|
-
async function (match, prefix, dq, sq, unq, suffix) {
|
|
1933
|
+
async function (/** @type {string} */ match, /** @type {string} */ prefix, /** @type {string | undefined} */ dq, /** @type {string | undefined} */ sq, /** @type {string | undefined} */ unq, /** @type {string} */ suffix) {
|
|
1813
1934
|
const quote = dq != null ? '"' : (sq != null ? "'" : '');
|
|
1814
1935
|
const url = dq ?? sq ?? unq ?? '';
|
|
1815
1936
|
try {
|
|
@@ -1834,8 +1955,8 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
1834
1955
|
: (inputCSS + '|' + type + '|' + cssSig);
|
|
1835
1956
|
|
|
1836
1957
|
try {
|
|
1837
|
-
const cached =
|
|
1838
|
-
if (cached) {
|
|
1958
|
+
const cached = cssCache.get(cssKey);
|
|
1959
|
+
if (cached !== undefined) {
|
|
1839
1960
|
// Support both resolved values and in-flight promises
|
|
1840
1961
|
return await cached;
|
|
1841
1962
|
}
|
|
@@ -1843,7 +1964,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
1843
1964
|
// In-flight promise caching: Prevent duplicate concurrent minifications
|
|
1844
1965
|
// of the same CSS content (same pattern as JS minification)
|
|
1845
1966
|
const inFlight = (async () => {
|
|
1846
|
-
const transformCSS = await
|
|
1967
|
+
const transformCSS = await cssLoader();
|
|
1847
1968
|
// Note: `Buffer.from()` is required—Lightning CSS API expects Uint8Array
|
|
1848
1969
|
const result = transformCSS({
|
|
1849
1970
|
filename: 'input.css',
|
|
@@ -1870,12 +1991,12 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
1870
1991
|
return (text.trim() && !outputCSS.trim() && (looksLikeTemplate || hasUID)) ? text : outputCSS;
|
|
1871
1992
|
})();
|
|
1872
1993
|
|
|
1873
|
-
|
|
1994
|
+
cssCache.set(cssKey, inFlight);
|
|
1874
1995
|
const resolved = await inFlight;
|
|
1875
|
-
|
|
1996
|
+
cssCache.set(cssKey, resolved);
|
|
1876
1997
|
return resolved;
|
|
1877
1998
|
} catch (err) {
|
|
1878
|
-
|
|
1999
|
+
cssCache.delete(cssKey);
|
|
1879
2000
|
if (!options.continueOnMinifyError) {
|
|
1880
2001
|
throw err;
|
|
1881
2002
|
}
|
|
@@ -1884,10 +2005,15 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
1884
2005
|
}
|
|
1885
2006
|
};
|
|
1886
2007
|
} else if (key === 'minifyJS' && typeof option !== 'function') {
|
|
1887
|
-
if (!option) {
|
|
2008
|
+
if (!option || !getTerser || !getSwc || !jsMinifyCache) {
|
|
1888
2009
|
return;
|
|
1889
2010
|
}
|
|
1890
2011
|
|
|
2012
|
+
// Capture to preserve TypeScript narrowing across the async closure boundary below
|
|
2013
|
+
const loadTerser = getTerser;
|
|
2014
|
+
const loadSwc = getSwc;
|
|
2015
|
+
const jsCache = jsMinifyCache;
|
|
2016
|
+
|
|
1891
2017
|
// Parse configuration
|
|
1892
2018
|
const config = typeof option === 'object' ? option : {};
|
|
1893
2019
|
const engine = (config.engine || 'terser').toLowerCase();
|
|
@@ -1922,7 +2048,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
1922
2048
|
cont: !!options.continueOnMinifyError
|
|
1923
2049
|
});
|
|
1924
2050
|
|
|
1925
|
-
options.minifyJS = async function (text, inline, isModule) {
|
|
2051
|
+
options.minifyJS = async function (/** @type {string} */ text, /** @type {boolean} */ inline, /** @type {boolean} */ isModule) {
|
|
1926
2052
|
const start = text.match(/^\s*<!--.*/);
|
|
1927
2053
|
const code = start ? text.slice(start[0].length).replace(/\n\s*-->\s*$/, '') : text;
|
|
1928
2054
|
|
|
@@ -1944,8 +2070,8 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
1944
2070
|
jsKey = (code.length > 2048 ? (hashContent(code) + '|') : (code + '|'))
|
|
1945
2071
|
+ (inline ? '1' : '0') + '|' + (isModule ? 'm' : '') + '|' + useEngine + '|' + optsSig;
|
|
1946
2072
|
|
|
1947
|
-
const cached =
|
|
1948
|
-
if (cached) {
|
|
2073
|
+
const cached = jsCache.get(jsKey);
|
|
2074
|
+
if (cached !== undefined) {
|
|
1949
2075
|
return await cached;
|
|
1950
2076
|
}
|
|
1951
2077
|
|
|
@@ -1961,11 +2087,11 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
1961
2087
|
},
|
|
1962
2088
|
...(isModule ? { module: true } : {}) // Overrides user options: module detection takes precedence for `<script type=module>`
|
|
1963
2089
|
};
|
|
1964
|
-
const terser = await
|
|
2090
|
+
const terser = await loadTerser();
|
|
1965
2091
|
const result = await terser(code, terserCallOptions);
|
|
1966
2092
|
return result.code.replace(RE_TRAILING_SEMICOLON, '');
|
|
1967
2093
|
} else if (useEngine === 'swc') {
|
|
1968
|
-
const swc = await
|
|
2094
|
+
const swc = await loadSwc();
|
|
1969
2095
|
// `swc.minify()` takes compress and mangle directly as options
|
|
1970
2096
|
const result = await swc.minify(code, {
|
|
1971
2097
|
compress: true,
|
|
@@ -1978,12 +2104,12 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
1978
2104
|
throw new Error(`Unknown JS minifier engine: ${useEngine}`);
|
|
1979
2105
|
})();
|
|
1980
2106
|
|
|
1981
|
-
|
|
2107
|
+
jsCache.set(jsKey, inFlight);
|
|
1982
2108
|
const resolved = await inFlight;
|
|
1983
|
-
|
|
2109
|
+
jsCache.set(jsKey, resolved);
|
|
1984
2110
|
return resolved;
|
|
1985
2111
|
} catch (err) {
|
|
1986
|
-
if (jsKey)
|
|
2112
|
+
if (jsKey) jsCache.delete(jsKey);
|
|
1987
2113
|
if (!options.continueOnMinifyError) {
|
|
1988
2114
|
throw err;
|
|
1989
2115
|
}
|
|
@@ -2009,7 +2135,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
2009
2135
|
// Create instance-specific cache (results depend on site configuration)
|
|
2010
2136
|
const instanceCache = new LRU(500);
|
|
2011
2137
|
|
|
2012
|
-
options.minifyURLs = function (text) {
|
|
2138
|
+
options.minifyURLs = function (/** @type {string} */ text) {
|
|
2013
2139
|
// Fast-path: Skip if text doesn’t look like a URL that needs processing
|
|
2014
2140
|
// Only process if contains URL-like characters (`/`, `:`, `#`, `?`) or spaces that need encoding
|
|
2015
2141
|
if (!/[/:?#\s]/.test(text)) {
|
|
@@ -2036,10 +2162,14 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
2036
2162
|
}
|
|
2037
2163
|
};
|
|
2038
2164
|
} else if (key === 'minifySVG' && typeof option !== 'function') {
|
|
2039
|
-
if (!option) {
|
|
2165
|
+
if (!option || !getSvgo || !svgMinifyCache) {
|
|
2040
2166
|
return;
|
|
2041
2167
|
}
|
|
2042
2168
|
|
|
2169
|
+
// Capture to preserve TypeScript narrowing across the async closure boundary below
|
|
2170
|
+
const loadSvgo = getSvgo;
|
|
2171
|
+
const svgCache = svgMinifyCache;
|
|
2172
|
+
|
|
2043
2173
|
const svgoOptions = typeof option === 'object' ? option : {};
|
|
2044
2174
|
|
|
2045
2175
|
// Pre-compute option signature for cache keys
|
|
@@ -2048,7 +2178,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
2048
2178
|
cont: !!options.continueOnMinifyError
|
|
2049
2179
|
});
|
|
2050
2180
|
|
|
2051
|
-
options.minifySVG = async function (svgContent) {
|
|
2181
|
+
options.minifySVG = async function (/** @type {string} */ svgContent) {
|
|
2052
2182
|
if (!svgContent || !svgContent.trim()) {
|
|
2053
2183
|
return svgContent;
|
|
2054
2184
|
}
|
|
@@ -2059,23 +2189,23 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
2059
2189
|
: (svgContent + '|' + svgSig);
|
|
2060
2190
|
|
|
2061
2191
|
try {
|
|
2062
|
-
const cached =
|
|
2063
|
-
if (cached) {
|
|
2192
|
+
const cached = svgCache.get(svgKey);
|
|
2193
|
+
if (cached !== undefined) {
|
|
2064
2194
|
return await cached;
|
|
2065
2195
|
}
|
|
2066
2196
|
|
|
2067
2197
|
const inFlight = (async () => {
|
|
2068
|
-
const optimize = await
|
|
2198
|
+
const optimize = await loadSvgo();
|
|
2069
2199
|
const result = optimize(svgContent, svgoOptions);
|
|
2070
2200
|
return result.data;
|
|
2071
2201
|
})();
|
|
2072
2202
|
|
|
2073
|
-
|
|
2203
|
+
svgCache.set(svgKey, inFlight);
|
|
2074
2204
|
const resolved = await inFlight;
|
|
2075
|
-
|
|
2205
|
+
svgCache.set(svgKey, resolved);
|
|
2076
2206
|
return resolved;
|
|
2077
2207
|
} catch (err) {
|
|
2078
|
-
|
|
2208
|
+
svgCache.delete(svgKey);
|
|
2079
2209
|
if (!options.continueOnMinifyError) {
|
|
2080
2210
|
throw err;
|
|
2081
2211
|
}
|
|
@@ -2099,11 +2229,16 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
2099
2229
|
return options;
|
|
2100
2230
|
};
|
|
2101
2231
|
|
|
2102
|
-
//
|
|
2232
|
+
// Type definitions
|
|
2103
2233
|
|
|
2234
|
+
/**
|
|
2235
|
+
* @typedef {{ name: string, value?: string | undefined, quote?: string, customAssign?: string, customOpen?: string, customClose?: string }} HTMLAttribute
|
|
2236
|
+
* Internal counterpart of the public typedef in `htmlminifier.js`—keep in sync.
|
|
2237
|
+
*/
|
|
2104
2238
|
|
|
2105
2239
|
// Lazy-load entities (used for `decodeEntities` and event-handler attribute decode before `minifyJS`)
|
|
2106
2240
|
|
|
2241
|
+
/** @type {Promise<Function> | undefined} */
|
|
2107
2242
|
let decodeHTMLStrictPromise;
|
|
2108
2243
|
async function getDecodeHTMLStrict() {
|
|
2109
2244
|
if (!decodeHTMLStrictPromise) {
|
|
@@ -2114,20 +2249,30 @@ async function getDecodeHTMLStrict() {
|
|
|
2114
2249
|
|
|
2115
2250
|
// Validators
|
|
2116
2251
|
|
|
2252
|
+
/**
|
|
2253
|
+
* @param {string} text
|
|
2254
|
+
* @param {{ignoreCustomComments: RegExp[]}} options
|
|
2255
|
+
*/
|
|
2117
2256
|
function isIgnoredComment(text, options) {
|
|
2118
|
-
|
|
2119
|
-
|
|
2257
|
+
// @@ 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
|
|
2258
|
+
if (!Array.isArray(options.ignoreCustomComments)) return false;
|
|
2259
|
+
for (const pattern of options.ignoreCustomComments) {
|
|
2260
|
+
if (pattern.test(text)) {
|
|
2120
2261
|
return true;
|
|
2121
2262
|
}
|
|
2122
2263
|
}
|
|
2123
2264
|
return false;
|
|
2124
2265
|
}
|
|
2125
2266
|
|
|
2267
|
+
/**
|
|
2268
|
+
* @param {string} attrName
|
|
2269
|
+
* @param {{customEventAttributes?: RegExp[]}} options
|
|
2270
|
+
*/
|
|
2126
2271
|
function isEventAttribute(attrName, options) {
|
|
2127
2272
|
const patterns = options.customEventAttributes;
|
|
2128
2273
|
if (patterns) {
|
|
2129
|
-
for (
|
|
2130
|
-
if (
|
|
2274
|
+
for (const pattern of patterns) {
|
|
2275
|
+
if (pattern.test(attrName)) {
|
|
2131
2276
|
return true;
|
|
2132
2277
|
}
|
|
2133
2278
|
}
|
|
@@ -2136,14 +2281,19 @@ function isEventAttribute(attrName, options) {
|
|
|
2136
2281
|
return RE_EVENT_ATTR_DEFAULT.test(attrName);
|
|
2137
2282
|
}
|
|
2138
2283
|
|
|
2284
|
+
/** @param {string} value */
|
|
2139
2285
|
function canRemoveAttributeQuotes(value) {
|
|
2140
2286
|
// https://mathiasbynens.be/notes/unquoted-attribute-values
|
|
2141
2287
|
return RE_CAN_REMOVE_ATTR_QUOTES.test(value);
|
|
2142
2288
|
}
|
|
2143
2289
|
|
|
2290
|
+
/**
|
|
2291
|
+
* @param {HTMLAttribute[]} attributes
|
|
2292
|
+
* @param {string} attribute
|
|
2293
|
+
*/
|
|
2144
2294
|
function attributesInclude(attributes, attribute) {
|
|
2145
|
-
for (
|
|
2146
|
-
if (
|
|
2295
|
+
for (const attr of attributes) {
|
|
2296
|
+
if (attr.name.toLowerCase() === attribute) {
|
|
2147
2297
|
return true;
|
|
2148
2298
|
}
|
|
2149
2299
|
}
|
|
@@ -2154,9 +2304,9 @@ function attributesInclude(attributes, attribute) {
|
|
|
2154
2304
|
* Remove duplicate attributes from an attribute list.
|
|
2155
2305
|
* Per HTML spec, when an attribute appears multiple times, the first occurrence wins.
|
|
2156
2306
|
* Duplicate attributes result in invalid HTML, so only the first is kept.
|
|
2157
|
-
* @param {
|
|
2307
|
+
* @param {HTMLAttribute[]} attrs - Array of attribute objects with `name` property
|
|
2158
2308
|
* @param {boolean} caseSensitive - Whether to compare names case-sensitively (for XML/SVG)
|
|
2159
|
-
* @returns {
|
|
2309
|
+
* @returns {HTMLAttribute[]} Deduplicated attribute array (modifies in place and returns)
|
|
2160
2310
|
*/
|
|
2161
2311
|
function deduplicateAttributes(attrs, caseSensitive) {
|
|
2162
2312
|
if (attrs.length < 2) {
|
|
@@ -2166,8 +2316,7 @@ function deduplicateAttributes(attrs, caseSensitive) {
|
|
|
2166
2316
|
const seen = new Set();
|
|
2167
2317
|
let writeIndex = 0;
|
|
2168
2318
|
|
|
2169
|
-
for (
|
|
2170
|
-
const attr = attrs[i];
|
|
2319
|
+
for (const attr of attrs) {
|
|
2171
2320
|
const key = caseSensitive ? attr.name : attr.name.toLowerCase();
|
|
2172
2321
|
|
|
2173
2322
|
if (!seen.has(key)) {
|
|
@@ -2180,6 +2329,12 @@ function deduplicateAttributes(attrs, caseSensitive) {
|
|
|
2180
2329
|
return attrs;
|
|
2181
2330
|
}
|
|
2182
2331
|
|
|
2332
|
+
/**
|
|
2333
|
+
* @param {string} tag
|
|
2334
|
+
* @param {string} attrName
|
|
2335
|
+
* @param {string} attrValue
|
|
2336
|
+
* @param {HTMLAttribute[]} attrs
|
|
2337
|
+
*/
|
|
2183
2338
|
function isAttributeRedundant(tag, attrName, attrValue, attrs) {
|
|
2184
2339
|
// Fast-path: Check if this element–attribute combination can possibly be redundant
|
|
2185
2340
|
// before doing expensive string operations
|
|
@@ -2213,32 +2368,35 @@ function isAttributeRedundant(tag, attrName, attrValue, attrs) {
|
|
|
2213
2368
|
}
|
|
2214
2369
|
|
|
2215
2370
|
// Check general defaults
|
|
2216
|
-
if (hasGeneralDefault && generalDefaults[attrName] === attrValue) {
|
|
2371
|
+
if (hasGeneralDefault && /** @type {Record<string, string>} */ (generalDefaults)[attrName] === attrValue) {
|
|
2217
2372
|
return true;
|
|
2218
2373
|
}
|
|
2219
2374
|
|
|
2220
2375
|
// Check tag-specific defaults
|
|
2221
|
-
return tagHasDefaults && tagDefaults[tag][attrName] === attrValue;
|
|
2376
|
+
return tagHasDefaults && /** @type {Record<string, string>} */ (/** @type {Record<string, unknown>} */ (tagDefaults)[tag])[attrName] === attrValue;
|
|
2222
2377
|
}
|
|
2223
2378
|
|
|
2224
2379
|
function isScriptTypeAttribute(attrValue = '') {
|
|
2225
|
-
attrValue = trimWhitespace(attrValue.split(/;/, 2)[0]).toLowerCase();
|
|
2380
|
+
attrValue = trimWhitespace(attrValue.split(/;/, 2)[0] ?? '').toLowerCase();
|
|
2226
2381
|
return attrValue === '' || executableScriptsMimetypes.has(attrValue);
|
|
2227
2382
|
}
|
|
2228
2383
|
|
|
2229
2384
|
function keepScriptTypeAttribute(attrValue = '') {
|
|
2230
|
-
attrValue = trimWhitespace(attrValue.split(/;/, 2)[0]).toLowerCase();
|
|
2385
|
+
attrValue = trimWhitespace(attrValue.split(/;/, 2)[0] ?? '').toLowerCase();
|
|
2231
2386
|
return keepScriptsMimetypes.has(attrValue);
|
|
2232
2387
|
}
|
|
2233
2388
|
|
|
2389
|
+
/**
|
|
2390
|
+
* @param {string} tag
|
|
2391
|
+
* @param {HTMLAttribute[]} attrs
|
|
2392
|
+
*/
|
|
2234
2393
|
function isExecutableScript(tag, attrs) {
|
|
2235
2394
|
if (tag !== 'script') {
|
|
2236
2395
|
return false;
|
|
2237
2396
|
}
|
|
2238
|
-
for (
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
return isScriptTypeAttribute(attrs[i].value);
|
|
2397
|
+
for (const attr of attrs) {
|
|
2398
|
+
if (attr.name.toLowerCase() === 'type') {
|
|
2399
|
+
return isScriptTypeAttribute(attr.value);
|
|
2242
2400
|
}
|
|
2243
2401
|
}
|
|
2244
2402
|
return true;
|
|
@@ -2249,23 +2407,30 @@ function isStyleLinkTypeAttribute(attrValue = '') {
|
|
|
2249
2407
|
return attrValue === '' || attrValue === 'text/css';
|
|
2250
2408
|
}
|
|
2251
2409
|
|
|
2410
|
+
/**
|
|
2411
|
+
* @param {string} tag
|
|
2412
|
+
* @param {HTMLAttribute[]} attrs
|
|
2413
|
+
*/
|
|
2252
2414
|
function isStyleElement(tag, attrs) {
|
|
2253
2415
|
if (tag !== 'style') {
|
|
2254
2416
|
return false;
|
|
2255
2417
|
}
|
|
2256
|
-
for (
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
return isStyleLinkTypeAttribute(attrs[i].value);
|
|
2418
|
+
for (const attr of attrs) {
|
|
2419
|
+
if (attr.name.toLowerCase() === 'type') {
|
|
2420
|
+
return isStyleLinkTypeAttribute(attr.value);
|
|
2260
2421
|
}
|
|
2261
2422
|
}
|
|
2262
2423
|
return true;
|
|
2263
2424
|
}
|
|
2264
2425
|
|
|
2426
|
+
/**
|
|
2427
|
+
* @param {string} attrName
|
|
2428
|
+
* @param {string} attrValue
|
|
2429
|
+
*/
|
|
2265
2430
|
function isBooleanAttribute(attrName, attrValue) {
|
|
2266
2431
|
return isSimpleBoolean.has(attrName) ||
|
|
2267
2432
|
(attrName === 'draggable' && !isBooleanValue.has(attrValue)) ||
|
|
2268
|
-
(collapsibleValues.has(attrName) && collapsibleValues.get(attrName)
|
|
2433
|
+
(collapsibleValues.has(attrName) && collapsibleValues.get(attrName)?.has(attrValue));
|
|
2269
2434
|
}
|
|
2270
2435
|
|
|
2271
2436
|
const uriTypeAttributes = new Map([
|
|
@@ -2285,6 +2450,10 @@ const uriTypeAttributes = new Map([
|
|
|
2285
2450
|
['script', new Set(['src', 'for'])]
|
|
2286
2451
|
]);
|
|
2287
2452
|
|
|
2453
|
+
/**
|
|
2454
|
+
* @param {string} attrName
|
|
2455
|
+
* @param {string} tag
|
|
2456
|
+
*/
|
|
2288
2457
|
function isUriTypeAttribute(attrName, tag) {
|
|
2289
2458
|
const set = uriTypeAttributes.get(tag);
|
|
2290
2459
|
return set ? set.has(attrName) : false;
|
|
@@ -2304,55 +2473,87 @@ const numberTypeAttributes = new Map([
|
|
|
2304
2473
|
['td', new Set(['rowspan', 'colspan'])]
|
|
2305
2474
|
]);
|
|
2306
2475
|
|
|
2476
|
+
/**
|
|
2477
|
+
* @param {string} attrName
|
|
2478
|
+
* @param {string} tag
|
|
2479
|
+
*/
|
|
2307
2480
|
function isNumberTypeAttribute(attrName, tag) {
|
|
2308
2481
|
const set = numberTypeAttributes.get(tag);
|
|
2309
2482
|
return set ? set.has(attrName) : false;
|
|
2310
2483
|
}
|
|
2311
2484
|
|
|
2485
|
+
/**
|
|
2486
|
+
* @param {string} tag
|
|
2487
|
+
* @param {HTMLAttribute[]} attrs
|
|
2488
|
+
* @param {string} value
|
|
2489
|
+
*/
|
|
2312
2490
|
function isLinkType(tag, attrs, value) {
|
|
2313
2491
|
if (tag !== 'link') return false;
|
|
2314
2492
|
const needle = String(value).toLowerCase();
|
|
2315
|
-
for (
|
|
2316
|
-
if (
|
|
2317
|
-
const tokens = String(
|
|
2493
|
+
for (const attr of attrs) {
|
|
2494
|
+
if (attr.name.toLowerCase() === 'rel') {
|
|
2495
|
+
const tokens = String(attr.value).toLowerCase().split(/\s+/);
|
|
2318
2496
|
if (tokens.includes(needle)) return true;
|
|
2319
2497
|
}
|
|
2320
2498
|
}
|
|
2321
2499
|
return false;
|
|
2322
2500
|
}
|
|
2323
2501
|
|
|
2502
|
+
/**
|
|
2503
|
+
* @param {string} tag
|
|
2504
|
+
* @param {HTMLAttribute[]} attrs
|
|
2505
|
+
* @param {string} attrName
|
|
2506
|
+
*/
|
|
2324
2507
|
function isMediaQuery(tag, attrs, attrName) {
|
|
2325
2508
|
return attrName === 'media' && (isLinkType(tag, attrs, 'stylesheet') || isStyleElement(tag, attrs));
|
|
2326
2509
|
}
|
|
2327
2510
|
|
|
2511
|
+
/**
|
|
2512
|
+
* @param {string} attrName
|
|
2513
|
+
* @param {string} tag
|
|
2514
|
+
*/
|
|
2328
2515
|
function isSrcset(attrName, tag) {
|
|
2329
2516
|
return attrName === 'srcset' && srcsetElements.has(tag);
|
|
2330
2517
|
}
|
|
2331
2518
|
|
|
2519
|
+
/**
|
|
2520
|
+
* @param {string} tag
|
|
2521
|
+
* @param {HTMLAttribute[]} attrs
|
|
2522
|
+
*/
|
|
2332
2523
|
function isMetaViewport(tag, attrs) {
|
|
2333
2524
|
if (tag !== 'meta') {
|
|
2334
2525
|
return false;
|
|
2335
2526
|
}
|
|
2336
|
-
for (
|
|
2337
|
-
if (
|
|
2527
|
+
for (const attr of attrs) {
|
|
2528
|
+
if (attr.name.toLowerCase() === 'name' && (attr.value || '').toLowerCase() === 'viewport') {
|
|
2338
2529
|
return true;
|
|
2339
2530
|
}
|
|
2340
2531
|
}
|
|
2341
2532
|
return false;
|
|
2342
2533
|
}
|
|
2343
2534
|
|
|
2535
|
+
/**
|
|
2536
|
+
* @param {string} tag
|
|
2537
|
+
* @param {HTMLAttribute[]} attrs
|
|
2538
|
+
*/
|
|
2344
2539
|
function isContentSecurityPolicy(tag, attrs) {
|
|
2345
2540
|
if (tag !== 'meta') {
|
|
2346
2541
|
return false;
|
|
2347
2542
|
}
|
|
2348
|
-
for (
|
|
2349
|
-
if (
|
|
2543
|
+
for (const attr of attrs) {
|
|
2544
|
+
if (attr.name.toLowerCase() === 'http-equiv' && (attr.value || '').toLowerCase() === 'content-security-policy') {
|
|
2350
2545
|
return true;
|
|
2351
2546
|
}
|
|
2352
2547
|
}
|
|
2353
2548
|
return false;
|
|
2354
2549
|
}
|
|
2355
2550
|
|
|
2551
|
+
/**
|
|
2552
|
+
* @param {string} tag
|
|
2553
|
+
* @param {string} attrName
|
|
2554
|
+
* @param {string | undefined} attrValue
|
|
2555
|
+
* @param {{removeEmptyAttributes?: boolean | Function}} options
|
|
2556
|
+
*/
|
|
2356
2557
|
function canDeleteEmptyAttribute(tag, attrName, attrValue, options) {
|
|
2357
2558
|
const isValueEmpty = !attrValue || attrValue.trim() === '';
|
|
2358
2559
|
if (!isValueEmpty) {
|
|
@@ -2364,9 +2565,13 @@ function canDeleteEmptyAttribute(tag, attrName, attrValue, options) {
|
|
|
2364
2565
|
return (tag === 'input' && attrName === 'value') || reEmptyAttribute.test(attrName);
|
|
2365
2566
|
}
|
|
2366
2567
|
|
|
2568
|
+
/**
|
|
2569
|
+
* @param {string} name
|
|
2570
|
+
* @param {HTMLAttribute[]} attrs
|
|
2571
|
+
*/
|
|
2367
2572
|
function hasAttrName(name, attrs) {
|
|
2368
|
-
for (
|
|
2369
|
-
if (
|
|
2573
|
+
for (const attr of attrs) {
|
|
2574
|
+
if (attr.name === name) {
|
|
2370
2575
|
return true;
|
|
2371
2576
|
}
|
|
2372
2577
|
}
|
|
@@ -2381,6 +2586,14 @@ const valueWhitespaceExemptElements = new Set(['button', 'data', 'input', 'optio
|
|
|
2381
2586
|
|
|
2382
2587
|
// Returns the cleaned attribute value directly (sync) or as a Promise (async);
|
|
2383
2588
|
// callers must handle both cases—use `isThenable()` to distinguish
|
|
2589
|
+
/**
|
|
2590
|
+
* @param {string} tag
|
|
2591
|
+
* @param {string} attrName
|
|
2592
|
+
* @param {string} attrValue
|
|
2593
|
+
* @param {Record<string, any>} options
|
|
2594
|
+
* @param {HTMLAttribute[]} attrs
|
|
2595
|
+
* @param {Function} minifyHTMLSelf
|
|
2596
|
+
*/
|
|
2384
2597
|
function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTMLSelf) {
|
|
2385
2598
|
const isEventAttr = isEventAttribute(attrName, options);
|
|
2386
2599
|
|
|
@@ -2404,9 +2617,9 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
|
|
|
2404
2617
|
return getDecodeHTMLStrict().then(decode => {
|
|
2405
2618
|
const decoded = decode(attrValue);
|
|
2406
2619
|
const result = options.minifyJS(decoded, true);
|
|
2407
|
-
const reEncode = v => (v && v.indexOf('&') !== -1) ? v.replace(RE_AMP_ENTITY, '&$1') : v;
|
|
2620
|
+
const reEncode = (/** @type {string} */ v) => (v && v.indexOf('&') !== -1) ? v.replace(RE_AMP_ENTITY, '&$1') : v;
|
|
2408
2621
|
if (isThenable(result)) {
|
|
2409
|
-
return result.then(reEncode, err => {
|
|
2622
|
+
return result.then(reEncode, (/** @type {Error} */ err) => {
|
|
2410
2623
|
if (!options.continueOnMinifyError) throw err;
|
|
2411
2624
|
options.log && options.log(err);
|
|
2412
2625
|
return attrValue;
|
|
@@ -2417,7 +2630,7 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
|
|
|
2417
2630
|
}
|
|
2418
2631
|
const result = options.minifyJS(attrValue, true);
|
|
2419
2632
|
if (isThenable(result)) {
|
|
2420
|
-
return result.catch(err => {
|
|
2633
|
+
return result.catch((/** @type {Error} */ err) => {
|
|
2421
2634
|
if (!options.continueOnMinifyError) throw err;
|
|
2422
2635
|
options.log && options.log(err);
|
|
2423
2636
|
return attrValue;
|
|
@@ -2444,8 +2657,8 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
|
|
|
2444
2657
|
const result = options.minifyURLs(attrValue);
|
|
2445
2658
|
if (isThenable(result)) {
|
|
2446
2659
|
return result
|
|
2447
|
-
.then(out => typeof out === 'string' ? out : attrValue)
|
|
2448
|
-
.catch(err => {
|
|
2660
|
+
.then((/** @type {unknown} */ out) => typeof out === 'string' ? out : attrValue)
|
|
2661
|
+
.catch((/** @type {Error} */ err) => {
|
|
2449
2662
|
if (!options.continueOnMinifyError) throw err;
|
|
2450
2663
|
options.log && options.log(err);
|
|
2451
2664
|
return attrValue;
|
|
@@ -2468,13 +2681,13 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
|
|
|
2468
2681
|
const cssResult = options.minifyCSS(attrValue, 'inline');
|
|
2469
2682
|
if (isThenable(cssResult)) {
|
|
2470
2683
|
return cssResult
|
|
2471
|
-
.then(minified => {
|
|
2684
|
+
.then((/** @type {string} */ minified) => {
|
|
2472
2685
|
// After minification, check if CSS consists entirely of invalid properties (no values)
|
|
2473
2686
|
// I.e., `color:` or `margin:;padding:` should be treated as empty
|
|
2474
2687
|
if (minified && /^(?:[a-z-]+:[;\s]*)+$/i.test(minified)) return '';
|
|
2475
2688
|
return minified;
|
|
2476
2689
|
})
|
|
2477
|
-
.catch(err => {
|
|
2690
|
+
.catch((/** @type {Error} */ err) => {
|
|
2478
2691
|
if (!options.continueOnMinifyError) throw err;
|
|
2479
2692
|
options.log && options.log(err);
|
|
2480
2693
|
return originalAttrValue;
|
|
@@ -2496,8 +2709,9 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
|
|
|
2496
2709
|
const match = candidate.match(/\s+([1-9][0-9]*w|[0-9]+(?:\.[0-9]+)?x)$/);
|
|
2497
2710
|
if (match) {
|
|
2498
2711
|
url = url.slice(0, -match[0].length);
|
|
2499
|
-
const
|
|
2500
|
-
const
|
|
2712
|
+
const group = match[1] ?? '';
|
|
2713
|
+
const num = +group.slice(0, -1);
|
|
2714
|
+
const suffix = group.slice(-1);
|
|
2501
2715
|
if (num !== 1 || suffix !== 'x') {
|
|
2502
2716
|
descriptor = ' ' + num + suffix;
|
|
2503
2717
|
}
|
|
@@ -2505,8 +2719,8 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
|
|
|
2505
2719
|
const out = options.minifyURLs(url);
|
|
2506
2720
|
if (isThenable(out)) {
|
|
2507
2721
|
return out
|
|
2508
|
-
.then(result => (typeof result === 'string' ? result : url) + descriptor)
|
|
2509
|
-
.catch(err => {
|
|
2722
|
+
.then((/** @type {unknown} */ result) => (typeof result === 'string' ? result : url) + descriptor)
|
|
2723
|
+
.catch((/** @type {Error} */ err) => {
|
|
2510
2724
|
if (!options.continueOnMinifyError) throw err;
|
|
2511
2725
|
options.log && options.log(err);
|
|
2512
2726
|
return url + descriptor;
|
|
@@ -2551,7 +2765,7 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
|
|
|
2551
2765
|
const originalAttrValue = attrValue;
|
|
2552
2766
|
const cssResult = options.minifyCSS(attrValue, 'media');
|
|
2553
2767
|
if (isThenable(cssResult)) {
|
|
2554
|
-
return cssResult.catch(err => {
|
|
2768
|
+
return cssResult.catch((/** @type {Error} */ err) => {
|
|
2555
2769
|
if (!options.continueOnMinifyError) throw err;
|
|
2556
2770
|
options.log && options.log(err);
|
|
2557
2771
|
return originalAttrValue;
|
|
@@ -2575,7 +2789,7 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
|
|
|
2575
2789
|
/**
|
|
2576
2790
|
* Choose appropriate quote character for an attribute value
|
|
2577
2791
|
* @param {string} attrValue - The attribute value
|
|
2578
|
-
* @param {
|
|
2792
|
+
* @param {{quoteCharacter?: string}} options - Minifier options
|
|
2579
2793
|
* @returns {string} The chosen quote character (`"` or `'`)
|
|
2580
2794
|
*/
|
|
2581
2795
|
function chooseAttributeQuote(attrValue, options) {
|
|
@@ -2594,6 +2808,13 @@ function chooseAttributeQuote(attrValue, options) {
|
|
|
2594
2808
|
|
|
2595
2809
|
// Returns the normalized attribute object directly (sync) or as a Promise (async);
|
|
2596
2810
|
// callers must handle both cases—use `isThenable()` to distinguish
|
|
2811
|
+
/**
|
|
2812
|
+
* @param {HTMLAttribute} attr
|
|
2813
|
+
* @param {HTMLAttribute[]} attrs
|
|
2814
|
+
* @param {string} tag
|
|
2815
|
+
* @param {Record<string, any>} options
|
|
2816
|
+
* @param {Function} minifyHTML
|
|
2817
|
+
*/
|
|
2597
2818
|
function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
2598
2819
|
const attrName = options.name(attr.name);
|
|
2599
2820
|
let attrValue = attr.value;
|
|
@@ -2609,9 +2830,18 @@ function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
2609
2830
|
}
|
|
2610
2831
|
|
|
2611
2832
|
// Internal: Handles attribute normalization after entity decoding (if any)
|
|
2833
|
+
/**
|
|
2834
|
+
* @param {string} attrName
|
|
2835
|
+
* @param {string | undefined} attrValue
|
|
2836
|
+
* @param {HTMLAttribute} attr
|
|
2837
|
+
* @param {HTMLAttribute[]} attrs
|
|
2838
|
+
* @param {string} tag
|
|
2839
|
+
* @param {Record<string, any>} options
|
|
2840
|
+
* @param {Function} minifyHTML
|
|
2841
|
+
*/
|
|
2612
2842
|
function normalizeAttrContinue(attrName, attrValue, attr, attrs, tag, options, minifyHTML) {
|
|
2613
2843
|
if ((options.removeRedundantAttributes &&
|
|
2614
|
-
isAttributeRedundant(tag, attrName, attrValue, attrs)) ||
|
|
2844
|
+
isAttributeRedundant(tag, attrName, attrValue ?? '', attrs)) ||
|
|
2615
2845
|
(options.removeScriptTypeAttributes && tag === 'script' &&
|
|
2616
2846
|
attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue)) ||
|
|
2617
2847
|
(options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
|
|
@@ -2622,7 +2852,7 @@ function normalizeAttrContinue(attrName, attrValue, attr, attrs, tag, options, m
|
|
|
2622
2852
|
if (attrValue) {
|
|
2623
2853
|
const cleaned = cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTML);
|
|
2624
2854
|
if (isThenable(cleaned)) {
|
|
2625
|
-
return cleaned.then(v => normalizeAttrFinish(attrName, v, attr, tag, options));
|
|
2855
|
+
return cleaned.then((/** @type {string | undefined} */ v) => normalizeAttrFinish(attrName, v, attr, tag, options));
|
|
2626
2856
|
}
|
|
2627
2857
|
return normalizeAttrFinish(attrName, cleaned, attr, tag, options);
|
|
2628
2858
|
}
|
|
@@ -2631,6 +2861,13 @@ function normalizeAttrContinue(attrName, attrValue, attr, attrs, tag, options, m
|
|
|
2631
2861
|
}
|
|
2632
2862
|
|
|
2633
2863
|
// Internal: Final checks and result assembly after value cleaning
|
|
2864
|
+
/**
|
|
2865
|
+
* @param {string} attrName
|
|
2866
|
+
* @param {string | undefined} attrValue
|
|
2867
|
+
* @param {HTMLAttribute} attr
|
|
2868
|
+
* @param {string} tag
|
|
2869
|
+
* @param {Record<string, any>} options
|
|
2870
|
+
*/
|
|
2634
2871
|
function normalizeAttrFinish(attrName, attrValue, attr, tag, options) {
|
|
2635
2872
|
if (options.removeEmptyAttributes &&
|
|
2636
2873
|
canDeleteEmptyAttribute(tag, attrName, attrValue, options)) {
|
|
@@ -2648,6 +2885,13 @@ function normalizeAttrFinish(attrName, attrValue, attr, tag, options) {
|
|
|
2648
2885
|
};
|
|
2649
2886
|
}
|
|
2650
2887
|
|
|
2888
|
+
/**
|
|
2889
|
+
* @param {{name: string, value?: string, attr: HTMLAttribute}} normalized
|
|
2890
|
+
* @param {string | boolean | undefined} hasUnarySlash
|
|
2891
|
+
* @param {Record<string, any>} options
|
|
2892
|
+
* @param {boolean} isLast
|
|
2893
|
+
* @param {string | undefined} uidAttr
|
|
2894
|
+
*/
|
|
2651
2895
|
function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
2652
2896
|
const attrName = normalized.name;
|
|
2653
2897
|
let attrValue = normalized.value;
|
|
@@ -2659,7 +2903,7 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
|
2659
2903
|
// Determine if need to add/keep quotes
|
|
2660
2904
|
const shouldAddQuotes = typeof attrValue !== 'undefined' && (
|
|
2661
2905
|
// If `removeAttributeQuotes` is enabled, add quotes only if they can’t be removed
|
|
2662
|
-
(options.removeAttributeQuotes && (attrValue.indexOf(uidAttr) !== -1 || !canRemoveAttributeQuotes(attrValue))) ||
|
|
2906
|
+
(options.removeAttributeQuotes && ((uidAttr ? attrValue.indexOf(uidAttr) !== -1 : false) || !canRemoveAttributeQuotes(attrValue))) ||
|
|
2663
2907
|
// If `removeAttributeQuotes` is not enabled, preserve original quote style or add quotes if value requires them
|
|
2664
2908
|
(!options.removeAttributeQuotes && (attrQuote !== '' || !canRemoveAttributeQuotes(attrValue) ||
|
|
2665
2909
|
// Special case: With `removeTagWhitespace`, unquoted values that aren’t last will have space added,
|
|
@@ -2668,6 +2912,7 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
|
2668
2912
|
);
|
|
2669
2913
|
|
|
2670
2914
|
if (shouldAddQuotes) {
|
|
2915
|
+
attrValue = attrValue ?? '';
|
|
2671
2916
|
// Determine the appropriate quote character
|
|
2672
2917
|
if (!options.preventAttributesEscaping) {
|
|
2673
2918
|
// Normal mode: Choose optimal quote type to minimize escaping
|
|
@@ -2769,11 +3014,20 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
|
2769
3014
|
return attr.customOpen + attrFragment + attr.customClose;
|
|
2770
3015
|
}
|
|
2771
3016
|
|
|
2772
|
-
|
|
3017
|
+
/** @import { HTMLAttribute } from './attributes.js' */
|
|
3018
|
+
|
|
3019
|
+
// Type definitions
|
|
2773
3020
|
|
|
3021
|
+
/**
|
|
3022
|
+
* @typedef {{ name: (str: string) => string, log?: Function }} MinifierOptions
|
|
3023
|
+
*/
|
|
2774
3024
|
|
|
2775
3025
|
// Tag omission rules
|
|
2776
3026
|
|
|
3027
|
+
/**
|
|
3028
|
+
* @param {string} optionalStartTag
|
|
3029
|
+
* @param {string} tag
|
|
3030
|
+
*/
|
|
2777
3031
|
function canRemoveParentTag(optionalStartTag, tag) {
|
|
2778
3032
|
switch (optionalStartTag) {
|
|
2779
3033
|
case 'html':
|
|
@@ -2789,6 +3043,10 @@ function canRemoveParentTag(optionalStartTag, tag) {
|
|
|
2789
3043
|
return false;
|
|
2790
3044
|
}
|
|
2791
3045
|
|
|
3046
|
+
/**
|
|
3047
|
+
* @param {string} optionalEndTag
|
|
3048
|
+
* @param {string} tag
|
|
3049
|
+
*/
|
|
2792
3050
|
function isStartTagMandatory(optionalEndTag, tag) {
|
|
2793
3051
|
switch (tag) {
|
|
2794
3052
|
case 'colgroup':
|
|
@@ -2799,6 +3057,10 @@ function isStartTagMandatory(optionalEndTag, tag) {
|
|
|
2799
3057
|
return false;
|
|
2800
3058
|
}
|
|
2801
3059
|
|
|
3060
|
+
/**
|
|
3061
|
+
* @param {string} optionalEndTag
|
|
3062
|
+
* @param {string} tag
|
|
3063
|
+
*/
|
|
2802
3064
|
function canRemovePrecedingTag(optionalEndTag, tag) {
|
|
2803
3065
|
switch (optionalEndTag) {
|
|
2804
3066
|
case 'html':
|
|
@@ -2838,6 +3100,10 @@ function canRemovePrecedingTag(optionalEndTag, tag) {
|
|
|
2838
3100
|
|
|
2839
3101
|
// Element removal logic
|
|
2840
3102
|
|
|
3103
|
+
/**
|
|
3104
|
+
* @param {string} tag
|
|
3105
|
+
* @param {HTMLAttribute[]} attrs
|
|
3106
|
+
*/
|
|
2841
3107
|
function canRemoveElement(tag, attrs) {
|
|
2842
3108
|
// Elements with `id` attribute must never be removed—they serve as:
|
|
2843
3109
|
// - Navigation targets (skip links, URL fragments)
|
|
@@ -2905,20 +3171,21 @@ function parseElementSpec(str, options) {
|
|
|
2905
3171
|
return null;
|
|
2906
3172
|
}
|
|
2907
3173
|
|
|
2908
|
-
const tag = options.name(match[1]);
|
|
2909
|
-
const attrString = match[2];
|
|
3174
|
+
const tag = options.name(match[1] ?? '');
|
|
3175
|
+
const attrString = match[2] ?? '';
|
|
2910
3176
|
|
|
2911
3177
|
if (!attrString.trim()) {
|
|
2912
3178
|
return { tag, attrs: null };
|
|
2913
3179
|
}
|
|
2914
3180
|
|
|
2915
3181
|
// Parse attributes from string
|
|
3182
|
+
/** @type {{[key: string]: string | undefined}} */
|
|
2916
3183
|
const attrs = {};
|
|
2917
3184
|
const attrRegex = /([a-zA-Z][\w:-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>/]+)))?/g;
|
|
2918
3185
|
let attrMatch;
|
|
2919
3186
|
|
|
2920
3187
|
while ((attrMatch = attrRegex.exec(attrString))) {
|
|
2921
|
-
const attrName = options.name(attrMatch[1]);
|
|
3188
|
+
const attrName = options.name(attrMatch[1] ?? '');
|
|
2922
3189
|
const attrValue = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4];
|
|
2923
3190
|
// Boolean attributes have no value (undefined)
|
|
2924
3191
|
attrs[attrName] = attrValue;
|
|
@@ -2940,7 +3207,7 @@ function parseRemoveEmptyElementsExcept(input, options) {
|
|
|
2940
3207
|
return [];
|
|
2941
3208
|
}
|
|
2942
3209
|
|
|
2943
|
-
return input.map(item => {
|
|
3210
|
+
return /** @type {Array<{tag: string, attrs: {[x: string]: string | undefined} | null}>} */ (input.map(item => {
|
|
2944
3211
|
if (typeof item === 'string') {
|
|
2945
3212
|
const spec = parseElementSpec(item, options);
|
|
2946
3213
|
if (!spec && options.log) {
|
|
@@ -2952,7 +3219,7 @@ function parseRemoveEmptyElementsExcept(input, options) {
|
|
|
2952
3219
|
options.log('Warning: “removeEmptyElementsExcept” specification must be a string, received: ' + typeof item);
|
|
2953
3220
|
}
|
|
2954
3221
|
return null;
|
|
2955
|
-
}).filter(Boolean);
|
|
3222
|
+
}).filter(Boolean));
|
|
2956
3223
|
}
|
|
2957
3224
|
|
|
2958
3225
|
/**
|
|
@@ -2995,261 +3262,29 @@ function shouldPreserveEmptyElement(tag, attrs, preserveList) {
|
|
|
2995
3262
|
return false;
|
|
2996
3263
|
}
|
|
2997
3264
|
|
|
2998
|
-
//
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
// Lazy-load heavy dependencies only when needed
|
|
3002
|
-
|
|
3003
|
-
let lightningCSSPromise;
|
|
3004
|
-
async function getLightningCSS() {
|
|
3005
|
-
if (!lightningCSSPromise) {
|
|
3006
|
-
lightningCSSPromise = import('lightningcss').then(m => m.transform);
|
|
3007
|
-
}
|
|
3008
|
-
return lightningCSSPromise;
|
|
3009
|
-
}
|
|
3010
|
-
|
|
3011
|
-
let terserPromise;
|
|
3012
|
-
async function getTerser() {
|
|
3013
|
-
if (!terserPromise) {
|
|
3014
|
-
terserPromise = import('terser').then(m => m.minify);
|
|
3015
|
-
}
|
|
3016
|
-
return terserPromise;
|
|
3017
|
-
}
|
|
3018
|
-
|
|
3019
|
-
let swcPromise;
|
|
3020
|
-
async function getSwc() {
|
|
3021
|
-
if (!swcPromise) {
|
|
3022
|
-
swcPromise = import('@swc/core')
|
|
3023
|
-
.then(m => m.default || m)
|
|
3024
|
-
.catch(() => {
|
|
3025
|
-
throw new Error(
|
|
3026
|
-
'The swc minifier requires @swc/core to be installed.\n' +
|
|
3027
|
-
'Install it with: npm install @swc/core'
|
|
3028
|
-
);
|
|
3029
|
-
});
|
|
3030
|
-
}
|
|
3031
|
-
return swcPromise;
|
|
3032
|
-
}
|
|
3033
|
-
|
|
3034
|
-
let svgoPromise;
|
|
3035
|
-
async function getSvgo() {
|
|
3036
|
-
if (!svgoPromise) {
|
|
3037
|
-
svgoPromise = import('svgo').then(m => m.optimize);
|
|
3038
|
-
}
|
|
3039
|
-
return svgoPromise;
|
|
3040
|
-
}
|
|
3041
|
-
|
|
3042
|
-
let decodeHTMLPromise;
|
|
3043
|
-
async function getDecodeHTML() {
|
|
3044
|
-
if (!decodeHTMLPromise) {
|
|
3045
|
-
decodeHTMLPromise = import('entities').then(m => m.decodeHTML);
|
|
3046
|
-
}
|
|
3047
|
-
return decodeHTMLPromise;
|
|
3048
|
-
}
|
|
3049
|
-
|
|
3050
|
-
// Minification caches (initialized on first use with configurable sizes)
|
|
3051
|
-
let cssMinifyCache = null;
|
|
3052
|
-
let jsMinifyCache = null;
|
|
3053
|
-
let svgMinifyCache = null;
|
|
3054
|
-
|
|
3055
|
-
// Pre-compiled patterns for script merging (avoid repeated allocation in hot path)
|
|
3056
|
-
const RE_SCRIPT_ATTRS = /([^\s=]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
3057
|
-
const RE_SCRIPT_OPEN = /<script(?=[\s>])/gi; // Finds tag start; use `findTagEnd()` for the actual closing `>`
|
|
3058
|
-
const RE_SCRIPT_CLOSE = /<\/script\s*>/gi;
|
|
3059
|
-
const SCRIPT_BOOL_ATTRS = new Set(['async', 'defer', 'nomodule']);
|
|
3060
|
-
const DEFAULT_JS_TYPES = new Set(['', 'text/javascript', 'application/javascript']);
|
|
3061
|
-
|
|
3062
|
-
// Pre-compiled patterns for buffer scanning
|
|
3063
|
-
const RE_START_TAG = /^<[^/!]/;
|
|
3064
|
-
const RE_END_TAG = /^<\//;
|
|
3065
|
-
|
|
3066
|
-
// Pre-compiled patterns for `htmlmin:ignore` block content analysis
|
|
3067
|
-
const RE_HTML_COMMENT_START = /^\s*<!--/;
|
|
3068
|
-
const RE_CLOSING_TAG_START = /^\s*<\/([a-zA-Z][\w:-]*)/;
|
|
3069
|
-
const RE_LAST_HTML_TAG = /[\s\S]*<(\/?[a-zA-Z][\w:-]*)/;
|
|
3070
|
-
|
|
3071
|
-
// HTML encoding types for annotation-xml (MathML)
|
|
3072
|
-
const RE_HTML_ENCODING = /^(text\/html|application\/xhtml\+xml)$/i;
|
|
3073
|
-
|
|
3074
|
-
// Script merging
|
|
3265
|
+
// Type definitions
|
|
3075
3266
|
|
|
3076
3267
|
/**
|
|
3077
|
-
*
|
|
3078
|
-
*
|
|
3079
|
-
*
|
|
3080
|
-
*
|
|
3081
|
-
* @
|
|
3268
|
+
* @typedef {Object} HTMLAttribute
|
|
3269
|
+
* Representation of an attribute from the HTML parser.
|
|
3270
|
+
* Internal counterpart: `HTMLAttribute` in `lib/attributes.js`—keep in sync.
|
|
3271
|
+
*
|
|
3272
|
+
* @prop {string} name
|
|
3273
|
+
* @prop {string} [value]
|
|
3274
|
+
* @prop {string} [quote]
|
|
3275
|
+
* @prop {string} [customAssign]
|
|
3276
|
+
* @prop {string} [customOpen]
|
|
3277
|
+
* @prop {string} [customClose]
|
|
3082
3278
|
*/
|
|
3083
|
-
function findTagEnd(html, pos) {
|
|
3084
|
-
let i = pos;
|
|
3085
|
-
while (i < html.length) {
|
|
3086
|
-
const ch = html[i];
|
|
3087
|
-
if (ch === '>') return i;
|
|
3088
|
-
if (ch === '"' || ch === "'") {
|
|
3089
|
-
const q = ch;
|
|
3090
|
-
i++;
|
|
3091
|
-
while (i < html.length && html[i] !== q) i++;
|
|
3092
|
-
}
|
|
3093
|
-
i++;
|
|
3094
|
-
}
|
|
3095
|
-
return -1;
|
|
3096
|
-
}
|
|
3097
3279
|
|
|
3098
3280
|
/**
|
|
3099
|
-
*
|
|
3100
|
-
*
|
|
3101
|
-
*
|
|
3102
|
-
* - Same `type` (or both default JavaScript)
|
|
3103
|
-
* - No conflicting attributes (`async`, `defer`, `nomodule`, different `nonce`)
|
|
3281
|
+
* @typedef {Object} MinifierOptions
|
|
3282
|
+
* Options that control how HTML is minified. All of these are optional
|
|
3283
|
+
* and usually default to a disabled/safe value unless noted.
|
|
3104
3284
|
*
|
|
3105
|
-
*
|
|
3106
|
-
*
|
|
3107
|
-
*
|
|
3108
|
-
*
|
|
3109
|
-
* @param {string} html - The HTML string to process
|
|
3110
|
-
* @returns {string} HTML with consecutive scripts merged
|
|
3111
|
-
*/
|
|
3112
|
-
function mergeConsecutiveScripts(html) {
|
|
3113
|
-
// Parse an attribute string into a name→value map
|
|
3114
|
-
const parseAttrs = (attrStr) => {
|
|
3115
|
-
const attrs = {};
|
|
3116
|
-
RE_SCRIPT_ATTRS.lastIndex = 0;
|
|
3117
|
-
let m;
|
|
3118
|
-
while ((m = RE_SCRIPT_ATTRS.exec(attrStr)) !== null) {
|
|
3119
|
-
const name = m[1].toLowerCase();
|
|
3120
|
-
const value = m[2] ?? m[3] ?? m[4] ?? '';
|
|
3121
|
-
attrs[name] = value;
|
|
3122
|
-
}
|
|
3123
|
-
return attrs;
|
|
3124
|
-
};
|
|
3125
|
-
|
|
3126
|
-
let changed = true;
|
|
3127
|
-
|
|
3128
|
-
// Keep merging until no more changes (handles chains of 3+ scripts)
|
|
3129
|
-
while (changed) {
|
|
3130
|
-
changed = false;
|
|
3131
|
-
RE_SCRIPT_OPEN.lastIndex = 0;
|
|
3132
|
-
let m1;
|
|
3133
|
-
|
|
3134
|
-
while ((m1 = RE_SCRIPT_OPEN.exec(html)) !== null) {
|
|
3135
|
-
// Use findTagEnd() to get the real closing '>', skipping quoted attribute values
|
|
3136
|
-
const tagEnd1 = findTagEnd(html, m1.index + 7);
|
|
3137
|
-
if (tagEnd1 === -1) break;
|
|
3138
|
-
|
|
3139
|
-
const attrs1Str = html.slice(m1.index + 7, tagEnd1);
|
|
3140
|
-
const contentStart1 = tagEnd1 + 1;
|
|
3141
|
-
|
|
3142
|
-
// Find end of this script’s content (first `</script>`—per HTML spec, raw text ends here)
|
|
3143
|
-
RE_SCRIPT_CLOSE.lastIndex = contentStart1;
|
|
3144
|
-
const close1 = RE_SCRIPT_CLOSE.exec(html);
|
|
3145
|
-
if (!close1) break;
|
|
3146
|
-
|
|
3147
|
-
const content1 = html.slice(contentStart1, close1.index);
|
|
3148
|
-
const afterClose1 = close1.index + close1[0].length;
|
|
3149
|
-
|
|
3150
|
-
// Skip optional whitespace and check for a consecutive <script> tag
|
|
3151
|
-
let i = afterClose1;
|
|
3152
|
-
while (i < html.length && (html[i] === ' ' || html[i] === '\t' || html[i] === '\n' || html[i] === '\r' || html[i] === '\f')) i++;
|
|
3153
|
-
if (html.slice(i, i + 7).toLowerCase() !== '<script' || (html[i + 7] !== '>' && !/\s/.test(html[i + 7]))) {
|
|
3154
|
-
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
3155
|
-
continue;
|
|
3156
|
-
}
|
|
3157
|
-
|
|
3158
|
-
const tagStart2 = i;
|
|
3159
|
-
const tagEnd2 = findTagEnd(html, tagStart2 + 7);
|
|
3160
|
-
if (tagEnd2 === -1) break;
|
|
3161
|
-
|
|
3162
|
-
const attrs2Str = html.slice(tagStart2 + 7, tagEnd2);
|
|
3163
|
-
const contentStart2 = tagEnd2 + 1;
|
|
3164
|
-
|
|
3165
|
-
// Find end of second script’s content
|
|
3166
|
-
RE_SCRIPT_CLOSE.lastIndex = contentStart2;
|
|
3167
|
-
const close2 = RE_SCRIPT_CLOSE.exec(html);
|
|
3168
|
-
if (!close2) break;
|
|
3169
|
-
|
|
3170
|
-
const content2 = html.slice(contentStart2, close2.index);
|
|
3171
|
-
const afterClose2 = close2.index + close2[0].length;
|
|
3172
|
-
|
|
3173
|
-
const a1 = parseAttrs(attrs1Str);
|
|
3174
|
-
const a2 = parseAttrs(attrs2Str);
|
|
3175
|
-
|
|
3176
|
-
// Check for `src`—cannot merge external scripts
|
|
3177
|
-
if ('src' in a1 || 'src' in a2) {
|
|
3178
|
-
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
3179
|
-
continue;
|
|
3180
|
-
}
|
|
3181
|
-
|
|
3182
|
-
// Check `type` compatibility (both must be default JS)
|
|
3183
|
-
// Non-JS types (modules, JSON, etc.) must not be merged:
|
|
3184
|
-
// Module scripts have per-script lexical scope, and non-JS content (e.g., JSON)
|
|
3185
|
-
// is not concatenable; even identical non-JS types are incompatible
|
|
3186
|
-
const type1 = (a1.type || '').toLowerCase();
|
|
3187
|
-
const type2 = (a2.type || '').toLowerCase();
|
|
3188
|
-
if (!DEFAULT_JS_TYPES.has(type1) || !DEFAULT_JS_TYPES.has(type2)) {
|
|
3189
|
-
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
3190
|
-
continue;
|
|
3191
|
-
}
|
|
3192
|
-
|
|
3193
|
-
// Check for conflicting boolean attributes
|
|
3194
|
-
let boolConflict = false;
|
|
3195
|
-
for (const attr of SCRIPT_BOOL_ATTRS) {
|
|
3196
|
-
if ((attr in a1) !== (attr in a2)) { boolConflict = true; break; }
|
|
3197
|
-
}
|
|
3198
|
-
|
|
3199
|
-
// Check `nonce`—must be same or both absent
|
|
3200
|
-
if (boolConflict || a1.nonce !== a2.nonce) {
|
|
3201
|
-
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
3202
|
-
continue;
|
|
3203
|
-
}
|
|
3204
|
-
|
|
3205
|
-
// Scripts are compatible—merge them
|
|
3206
|
-
changed = true;
|
|
3207
|
-
|
|
3208
|
-
// Combine content—use semicolon normally, newline only for trailing `//` comments
|
|
3209
|
-
const c1 = content1.trim();
|
|
3210
|
-
const c2 = content2.trim();
|
|
3211
|
-
let mergedContent;
|
|
3212
|
-
if (c1 && c2) {
|
|
3213
|
-
// Check if last line of c1 contains `//` (single-line comment)
|
|
3214
|
-
// If so, use newline to terminate it; otherwise use semicolon (if not already present)
|
|
3215
|
-
const lastLine = c1.slice(c1.lastIndexOf('\n') + 1);
|
|
3216
|
-
const separator = lastLine.includes('//') ? '\n' : (c1.endsWith(';') ? '' : ';');
|
|
3217
|
-
mergedContent = c1 + separator + c2;
|
|
3218
|
-
} else {
|
|
3219
|
-
mergedContent = c1 || c2;
|
|
3220
|
-
}
|
|
3221
|
-
|
|
3222
|
-
// Use first script’s attributes (they should be compatible)
|
|
3223
|
-
html = html.slice(0, m1.index) + `<script${attrs1Str}>${mergedContent}</script>` + html.slice(afterClose2);
|
|
3224
|
-
break; // Restart scanning (outer while loop)
|
|
3225
|
-
}
|
|
3226
|
-
}
|
|
3227
|
-
|
|
3228
|
-
return html;
|
|
3229
|
-
}
|
|
3230
|
-
|
|
3231
|
-
// Type definitions
|
|
3232
|
-
|
|
3233
|
-
/**
|
|
3234
|
-
* @typedef {Object} HTMLAttribute
|
|
3235
|
-
* Representation of an attribute from the HTML parser.
|
|
3236
|
-
*
|
|
3237
|
-
* @prop {string} name
|
|
3238
|
-
* @prop {string} [value]
|
|
3239
|
-
* @prop {string} [quote]
|
|
3240
|
-
* @prop {string} [customAssign]
|
|
3241
|
-
* @prop {string} [customOpen]
|
|
3242
|
-
* @prop {string} [customClose]
|
|
3243
|
-
*/
|
|
3244
|
-
|
|
3245
|
-
/**
|
|
3246
|
-
* @typedef {Object} MinifierOptions
|
|
3247
|
-
* Options that control how HTML is minified. All of these are optional
|
|
3248
|
-
* and usually default to a disabled/safe value unless noted.
|
|
3249
|
-
*
|
|
3250
|
-
* @prop {(tag: string, attrs: HTMLAttribute[], canCollapseWhitespace: (tag: string) => boolean) => boolean} [canCollapseWhitespace]
|
|
3251
|
-
* Predicate that determines whether whitespace inside a given element
|
|
3252
|
-
* can be collapsed.
|
|
3285
|
+
* @prop {(tag: string, attrs: HTMLAttribute[], canCollapseWhitespace: (tag: string) => boolean) => boolean} [canCollapseWhitespace]
|
|
3286
|
+
* Predicate that determines whether whitespace inside a given element
|
|
3287
|
+
* can be collapsed.
|
|
3253
3288
|
*
|
|
3254
3289
|
* Default: Built-in `canCollapseWhitespace` function
|
|
3255
3290
|
*
|
|
@@ -3642,23 +3677,275 @@ function mergeConsecutiveScripts(html) {
|
|
|
3642
3677
|
* See also: https://perfectionkills.com/experimenting-with-html-minifier/#use_short_doctype
|
|
3643
3678
|
*
|
|
3644
3679
|
* Default: `false`
|
|
3680
|
+
*
|
|
3681
|
+
* @prop {boolean} [insideSVG] - Internal: Set when inside SVG/MathML context
|
|
3682
|
+
* @prop {boolean} [insideForeignContent] - Internal: Set when inside SVG `foreignObject`
|
|
3683
|
+
* @prop {Function} [parentName] - Internal: Preserved name function during namespace transitions
|
|
3684
|
+
* @prop {Function} [htmlName] - Internal: HTML name function preserved from outer context
|
|
3645
3685
|
*/
|
|
3646
3686
|
|
|
3687
|
+
// Lazy-load heavy dependencies only when needed
|
|
3688
|
+
|
|
3689
|
+
/** @type {Promise<Function> | undefined} */
|
|
3690
|
+
let lightningCSSPromise;
|
|
3691
|
+
async function getLightningCSS() {
|
|
3692
|
+
if (!lightningCSSPromise) {
|
|
3693
|
+
lightningCSSPromise = import('lightningcss').then(m => m.transform);
|
|
3694
|
+
}
|
|
3695
|
+
return lightningCSSPromise;
|
|
3696
|
+
}
|
|
3697
|
+
|
|
3698
|
+
/** @type {Promise<Function> | undefined} */
|
|
3699
|
+
let terserPromise;
|
|
3700
|
+
async function getTerser() {
|
|
3701
|
+
if (!terserPromise) {
|
|
3702
|
+
terserPromise = import('terser').then(m => m.minify);
|
|
3703
|
+
}
|
|
3704
|
+
return terserPromise;
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3707
|
+
/** @type {Promise<any> | undefined} */
|
|
3708
|
+
let swcPromise;
|
|
3709
|
+
async function getSwc() {
|
|
3710
|
+
if (!swcPromise) {
|
|
3711
|
+
swcPromise = import('@swc/core')
|
|
3712
|
+
.then(m => m.default || m)
|
|
3713
|
+
.catch(() => {
|
|
3714
|
+
throw new Error(
|
|
3715
|
+
'The swc minifier requires @swc/core to be installed.\n' +
|
|
3716
|
+
'Install it with: npm install @swc/core'
|
|
3717
|
+
);
|
|
3718
|
+
});
|
|
3719
|
+
}
|
|
3720
|
+
return swcPromise;
|
|
3721
|
+
}
|
|
3722
|
+
|
|
3723
|
+
/** @type {Promise<Function> | undefined} */
|
|
3724
|
+
let svgoPromise;
|
|
3725
|
+
async function getSvgo() {
|
|
3726
|
+
if (!svgoPromise) {
|
|
3727
|
+
svgoPromise = import('svgo').then(m => m.optimize);
|
|
3728
|
+
}
|
|
3729
|
+
return svgoPromise;
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3732
|
+
/** @type {Promise<Function> | undefined} */
|
|
3733
|
+
let decodeHTMLPromise;
|
|
3734
|
+
async function getDecodeHTML() {
|
|
3735
|
+
if (!decodeHTMLPromise) {
|
|
3736
|
+
decodeHTMLPromise = import('entities').then(m => m.decodeHTML);
|
|
3737
|
+
}
|
|
3738
|
+
return decodeHTMLPromise;
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
// Minification caches (initialized on first use with configurable sizes)
|
|
3742
|
+
/** @type {LRU | null} */
|
|
3743
|
+
let cssMinifyCache = null;
|
|
3744
|
+
/** @type {LRU | null} */
|
|
3745
|
+
let jsMinifyCache = null;
|
|
3746
|
+
/** @type {LRU | null} */
|
|
3747
|
+
let svgMinifyCache = null;
|
|
3748
|
+
|
|
3749
|
+
// Pre-compiled patterns for script merging (avoid repeated allocation in hot path)
|
|
3750
|
+
const RE_SCRIPT_ATTRS = /([^\s=]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
3751
|
+
const RE_SCRIPT_OPEN = /<script(?=[\s>])/gi; // Finds tag start; use `findTagEnd()` for the actual closing `>`
|
|
3752
|
+
const RE_SCRIPT_CLOSE = /<\/script\s*>/gi;
|
|
3753
|
+
const SCRIPT_BOOL_ATTRS = new Set(['async', 'defer', 'nomodule']);
|
|
3754
|
+
const DEFAULT_JS_TYPES = new Set(['', 'text/javascript', 'application/javascript']);
|
|
3755
|
+
|
|
3756
|
+
// Pre-compiled patterns for buffer scanning
|
|
3757
|
+
const RE_START_TAG = /^<[^/!]/;
|
|
3758
|
+
const RE_END_TAG = /^<\//;
|
|
3759
|
+
|
|
3760
|
+
// Pre-compiled patterns for `htmlmin:ignore` block content analysis
|
|
3761
|
+
const RE_HTML_COMMENT_START = /^\s*<!--/;
|
|
3762
|
+
const RE_CLOSING_TAG_START = /^\s*<\/([a-zA-Z][\w:-]*)/;
|
|
3763
|
+
const RE_LAST_HTML_TAG = /[\s\S]*<(\/?[a-zA-Z][\w:-]*)/;
|
|
3764
|
+
|
|
3765
|
+
// HTML encoding types for annotation-xml (MathML)
|
|
3766
|
+
const RE_HTML_ENCODING = /^(text\/html|application\/xhtml\+xml)$/i;
|
|
3767
|
+
|
|
3768
|
+
// Script merging
|
|
3769
|
+
|
|
3770
|
+
/**
|
|
3771
|
+
* Find the index of the `>` that closes an opening tag, correctly skipping
|
|
3772
|
+
* over quoted attribute values (which may contain `>`).
|
|
3773
|
+
* @param {string} html
|
|
3774
|
+
* @param {number} pos - Start position (just after the tag name)
|
|
3775
|
+
* @returns {number} Index of the closing `>`, or -1 if not found
|
|
3776
|
+
*/
|
|
3777
|
+
function findTagEnd(html, pos) {
|
|
3778
|
+
let i = pos;
|
|
3779
|
+
while (i < html.length) {
|
|
3780
|
+
const ch = html[i];
|
|
3781
|
+
if (ch === '>') return i;
|
|
3782
|
+
if (ch === '"' || ch === "'") {
|
|
3783
|
+
const q = ch;
|
|
3784
|
+
i++;
|
|
3785
|
+
while (i < html.length && html[i] !== q) i++;
|
|
3786
|
+
}
|
|
3787
|
+
i++;
|
|
3788
|
+
}
|
|
3789
|
+
return -1;
|
|
3790
|
+
}
|
|
3791
|
+
|
|
3792
|
+
/**
|
|
3793
|
+
* Merge consecutive inline script tags into one (`mergeConsecutiveScripts`).
|
|
3794
|
+
* Only merges scripts that are compatible:
|
|
3795
|
+
* - Both inline (no `src` attribute)
|
|
3796
|
+
* - Same `type` (or both default JavaScript)
|
|
3797
|
+
* - No conflicting attributes (`async`, `defer`, `nomodule`, different `nonce`)
|
|
3798
|
+
*
|
|
3799
|
+
* Uses a scanner rather than a regex to locate script boundaries, so literal
|
|
3800
|
+
* `</script>` strings inside script content are handled correctly per the HTML
|
|
3801
|
+
* spec (raw text ends at the first `</script>`).
|
|
3802
|
+
*
|
|
3803
|
+
* @param {string} html - The HTML string to process
|
|
3804
|
+
* @returns {string} HTML with consecutive scripts merged
|
|
3805
|
+
*/
|
|
3806
|
+
function mergeConsecutiveScripts(html) {
|
|
3807
|
+
// Parse an attribute string into a name→value map
|
|
3808
|
+
const parseAttrs = (/** @type {string} */ attrStr) => {
|
|
3809
|
+
/** @type {Record<string, string>} */
|
|
3810
|
+
const attrs = {};
|
|
3811
|
+
RE_SCRIPT_ATTRS.lastIndex = 0;
|
|
3812
|
+
let m;
|
|
3813
|
+
while ((m = RE_SCRIPT_ATTRS.exec(attrStr)) !== null) {
|
|
3814
|
+
const name = (m[1] ?? '').toLowerCase();
|
|
3815
|
+
const value = m[2] ?? m[3] ?? m[4] ?? '';
|
|
3816
|
+
attrs[name] = value;
|
|
3817
|
+
}
|
|
3818
|
+
return attrs;
|
|
3819
|
+
};
|
|
3820
|
+
|
|
3821
|
+
let changed = true;
|
|
3822
|
+
|
|
3823
|
+
// Keep merging until no more changes (handles chains of 3+ scripts)
|
|
3824
|
+
while (changed) {
|
|
3825
|
+
changed = false;
|
|
3826
|
+
RE_SCRIPT_OPEN.lastIndex = 0;
|
|
3827
|
+
let m1;
|
|
3828
|
+
|
|
3829
|
+
while ((m1 = RE_SCRIPT_OPEN.exec(html)) !== null) {
|
|
3830
|
+
// Use findTagEnd() to get the real closing '>', skipping quoted attribute values
|
|
3831
|
+
const tagEnd1 = findTagEnd(html, m1.index + 7);
|
|
3832
|
+
if (tagEnd1 === -1) break;
|
|
3833
|
+
|
|
3834
|
+
const attrs1Str = html.slice(m1.index + 7, tagEnd1);
|
|
3835
|
+
const contentStart1 = tagEnd1 + 1;
|
|
3836
|
+
|
|
3837
|
+
// Find end of this script’s content (first `</script>`—per HTML spec, raw text ends here)
|
|
3838
|
+
RE_SCRIPT_CLOSE.lastIndex = contentStart1;
|
|
3839
|
+
const close1 = RE_SCRIPT_CLOSE.exec(html);
|
|
3840
|
+
if (!close1) break;
|
|
3841
|
+
|
|
3842
|
+
const content1 = html.slice(contentStart1, close1.index);
|
|
3843
|
+
const afterClose1 = close1.index + close1[0].length;
|
|
3844
|
+
|
|
3845
|
+
// Skip optional whitespace and check for a consecutive <script> tag
|
|
3846
|
+
let i = afterClose1;
|
|
3847
|
+
while (i < html.length && (html[i] === ' ' || html[i] === '\t' || html[i] === '\n' || html[i] === '\r' || html[i] === '\f')) i++;
|
|
3848
|
+
if (html.slice(i, i + 7).toLowerCase() !== '<script' || (html.charAt(i + 7) !== '>' && !/\s/.test(html.charAt(i + 7)))) {
|
|
3849
|
+
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
3850
|
+
continue;
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
const tagStart2 = i;
|
|
3854
|
+
const tagEnd2 = findTagEnd(html, tagStart2 + 7);
|
|
3855
|
+
if (tagEnd2 === -1) break;
|
|
3856
|
+
|
|
3857
|
+
const attrs2Str = html.slice(tagStart2 + 7, tagEnd2);
|
|
3858
|
+
const contentStart2 = tagEnd2 + 1;
|
|
3859
|
+
|
|
3860
|
+
// Find end of second script’s content
|
|
3861
|
+
RE_SCRIPT_CLOSE.lastIndex = contentStart2;
|
|
3862
|
+
const close2 = RE_SCRIPT_CLOSE.exec(html);
|
|
3863
|
+
if (!close2) break;
|
|
3864
|
+
|
|
3865
|
+
const content2 = html.slice(contentStart2, close2.index);
|
|
3866
|
+
const afterClose2 = close2.index + close2[0].length;
|
|
3867
|
+
|
|
3868
|
+
const a1 = parseAttrs(attrs1Str);
|
|
3869
|
+
const a2 = parseAttrs(attrs2Str);
|
|
3870
|
+
|
|
3871
|
+
// Check for `src`—cannot merge external scripts
|
|
3872
|
+
if ('src' in a1 || 'src' in a2) {
|
|
3873
|
+
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
3874
|
+
continue;
|
|
3875
|
+
}
|
|
3876
|
+
|
|
3877
|
+
// Check `type` compatibility (both must be default JS)
|
|
3878
|
+
// Non-JS types (modules, JSON, etc.) must not be merged:
|
|
3879
|
+
// Module scripts have per-script lexical scope, and non-JS content (e.g., JSON)
|
|
3880
|
+
// is not concatenable; even identical non-JS types are incompatible
|
|
3881
|
+
const type1 = (a1.type || '').toLowerCase();
|
|
3882
|
+
const type2 = (a2.type || '').toLowerCase();
|
|
3883
|
+
if (!DEFAULT_JS_TYPES.has(type1) || !DEFAULT_JS_TYPES.has(type2)) {
|
|
3884
|
+
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
3885
|
+
continue;
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
// Check for conflicting boolean attributes
|
|
3889
|
+
let boolConflict = false;
|
|
3890
|
+
for (const attr of SCRIPT_BOOL_ATTRS) {
|
|
3891
|
+
if ((attr in a1) !== (attr in a2)) { boolConflict = true; break; }
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
// Check `nonce`—must be same or both absent
|
|
3895
|
+
if (boolConflict || a1.nonce !== a2.nonce) {
|
|
3896
|
+
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
3897
|
+
continue;
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
// Scripts are compatible—merge them
|
|
3901
|
+
changed = true;
|
|
3902
|
+
|
|
3903
|
+
// Combine content—use semicolon normally, newline only for trailing `//` comments
|
|
3904
|
+
const c1 = content1.trim();
|
|
3905
|
+
const c2 = content2.trim();
|
|
3906
|
+
let mergedContent;
|
|
3907
|
+
if (c1 && c2) {
|
|
3908
|
+
// Check if last line of c1 contains `//` (single-line comment)
|
|
3909
|
+
// If so, use newline to terminate it; otherwise use semicolon (if not already present)
|
|
3910
|
+
const lastLine = c1.slice(c1.lastIndexOf('\n') + 1);
|
|
3911
|
+
const separator = lastLine.includes('//') ? '\n' : (c1.endsWith(';') ? '' : ';');
|
|
3912
|
+
mergedContent = c1 + separator + c2;
|
|
3913
|
+
} else {
|
|
3914
|
+
mergedContent = c1 || c2;
|
|
3915
|
+
}
|
|
3916
|
+
|
|
3917
|
+
// Use first script’s attributes (they should be compatible)
|
|
3918
|
+
html = html.slice(0, m1.index) + `<script${attrs1Str}>${mergedContent}</script>` + html.slice(afterClose2);
|
|
3919
|
+
break; // Restart scanning (outer while loop)
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
|
|
3923
|
+
return html;
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
/**
|
|
3927
|
+
* @param {string} value
|
|
3928
|
+
* @param {MinifierOptions} options
|
|
3929
|
+
* @param {string} uidIgnore
|
|
3930
|
+
* @param {string} uidAttr
|
|
3931
|
+
* @param {string[]} ignoredMarkupChunks
|
|
3932
|
+
*/
|
|
3647
3933
|
async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupChunks) {
|
|
3648
3934
|
const attrChains = options.sortAttributes && typeof options.sortAttributes !== 'function' && Object.create(null);
|
|
3649
3935
|
const classChain = options.sortClassNames && typeof options.sortClassNames !== 'function' && new TokenChain();
|
|
3936
|
+
const resolveName = /** @type {(name: string) => string} */ (options.name);
|
|
3650
3937
|
|
|
3651
|
-
function attrNames(attrs) {
|
|
3938
|
+
function attrNames(/** @type {HTMLAttribute[]} */ attrs) {
|
|
3652
3939
|
return attrs.map(function (attr) {
|
|
3653
|
-
return
|
|
3940
|
+
return resolveName(attr.name);
|
|
3654
3941
|
});
|
|
3655
3942
|
}
|
|
3656
3943
|
|
|
3657
|
-
function shouldSkipUID(token, uid) {
|
|
3944
|
+
function shouldSkipUID(/** @type {string} */ token, /** @type {string} */ uid) {
|
|
3658
3945
|
return !uid || token.indexOf(uid) === -1;
|
|
3659
3946
|
}
|
|
3660
3947
|
|
|
3661
|
-
function shouldKeepToken(token) {
|
|
3948
|
+
function shouldKeepToken(/** @type {string} */ token) {
|
|
3662
3949
|
// Filter out any HTML comment tokens (UID placeholders)
|
|
3663
3950
|
// These are temporary markers created by `htmlmin:ignore` and `ignoreCustomFragments`
|
|
3664
3951
|
if (token.startsWith('<!--') && token.endsWith('-->')) {
|
|
@@ -3672,10 +3959,13 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
3672
3959
|
const whitespaceSplitPatternScan = /[ \t\n\f\r]+/;
|
|
3673
3960
|
const whitespaceSplitPatternSort = /[ \n\f\r]+/;
|
|
3674
3961
|
|
|
3675
|
-
async function scan(input) {
|
|
3676
|
-
|
|
3962
|
+
async function scan(/** @type {string} */ input) {
|
|
3963
|
+
/** @type {string} */
|
|
3964
|
+
let currentTag;
|
|
3965
|
+
/** @type {string | undefined} */
|
|
3966
|
+
let currentType;
|
|
3677
3967
|
const parser = new HTMLParser(input, {
|
|
3678
|
-
start: function (tag, attrs) {
|
|
3968
|
+
start: function (/** @type {string} */ tag, /** @type {HTMLAttribute[]} */ attrs) {
|
|
3679
3969
|
if (attrChains) {
|
|
3680
3970
|
if (!attrChains[tag]) {
|
|
3681
3971
|
attrChains[tag] = new TokenChain();
|
|
@@ -3683,9 +3973,8 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
3683
3973
|
const attrNamesList = attrNames(attrs).filter(shouldKeepToken);
|
|
3684
3974
|
attrChains[tag].add(attrNamesList);
|
|
3685
3975
|
}
|
|
3686
|
-
for (
|
|
3687
|
-
|
|
3688
|
-
if (classChain && attr.value && options.name(attr.name) === 'class') {
|
|
3976
|
+
for (const attr of attrs) {
|
|
3977
|
+
if (classChain && attr.value && resolveName(attr.name) === 'class') {
|
|
3689
3978
|
const classes = trimWhitespace(attr.value).split(whitespaceSplitPatternScan).filter(shouldKeepToken);
|
|
3690
3979
|
classChain.add(classes);
|
|
3691
3980
|
} else if (options.processScripts && attr.name.toLowerCase() === 'type') {
|
|
@@ -3697,11 +3986,11 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
3697
3986
|
end: function () {
|
|
3698
3987
|
currentTag = '';
|
|
3699
3988
|
},
|
|
3700
|
-
chars: async function (text) {
|
|
3989
|
+
chars: async function (/** @type {string} */ text) {
|
|
3701
3990
|
// Only recursively scan HTML content, not JSON-LD or other non-HTML script types
|
|
3702
3991
|
// `scan()` is for analyzing HTML attribute order, not for parsing JSON
|
|
3703
3992
|
if (options.processScripts && specialContentElements.has(currentTag) &&
|
|
3704
|
-
options.processScripts.indexOf(currentType) > -1 &&
|
|
3993
|
+
options.processScripts.indexOf(currentType ?? '') > -1 &&
|
|
3705
3994
|
currentType === 'text/html') {
|
|
3706
3995
|
await scan(text);
|
|
3707
3996
|
}
|
|
@@ -3760,7 +4049,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
3760
4049
|
// Expand UID tokens back to the original content for frequency analysis
|
|
3761
4050
|
let expandedValue = value;
|
|
3762
4051
|
if (uidReplacePattern) {
|
|
3763
|
-
expandedValue = value.replace(uidReplacePattern, function (
|
|
4052
|
+
expandedValue = value.replace(uidReplacePattern, function (/** @type {string} */ _match, /** @type {string} */ index) {
|
|
3764
4053
|
return ignoredMarkupChunks[+index] || '';
|
|
3765
4054
|
});
|
|
3766
4055
|
// Reset `lastIndex` for pattern reuse
|
|
@@ -3781,7 +4070,11 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
3781
4070
|
await scan(scanValue);
|
|
3782
4071
|
} finally {
|
|
3783
4072
|
// Restore original option
|
|
3784
|
-
|
|
4073
|
+
if (originalContinueOnParseError !== undefined) {
|
|
4074
|
+
options.continueOnParseError = originalContinueOnParseError;
|
|
4075
|
+
} else {
|
|
4076
|
+
delete options.continueOnParseError;
|
|
4077
|
+
}
|
|
3785
4078
|
}
|
|
3786
4079
|
if (attrChains) {
|
|
3787
4080
|
const attrSorters = Object.create(null);
|
|
@@ -3791,14 +4084,14 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
3791
4084
|
// Memoize sorted attribute orders—attribute sets often repeat in templates
|
|
3792
4085
|
const attrOrderCache = new LRU(500);
|
|
3793
4086
|
|
|
3794
|
-
options.sortAttributes = function (tag, attrs) {
|
|
4087
|
+
options.sortAttributes = function (/** @type {string} */ tag, /** @type {HTMLAttribute[]} */ attrs) {
|
|
3795
4088
|
const sorter = attrSorters[tag];
|
|
3796
4089
|
if (sorter) {
|
|
3797
4090
|
const names = attrNames(attrs);
|
|
3798
4091
|
|
|
3799
4092
|
// Create order-independent cache key from tag and sorted attribute names
|
|
3800
4093
|
const cacheKey = tag + ':' + names.slice().sort().join(',');
|
|
3801
|
-
let sortedNames = attrOrderCache.get(cacheKey);
|
|
4094
|
+
let sortedNames = /** @type {string[] | undefined} */ (attrOrderCache.get(cacheKey));
|
|
3802
4095
|
|
|
3803
4096
|
if (sortedNames === undefined) {
|
|
3804
4097
|
// Only sort if not in cache—need to clone names since sort mutates in place
|
|
@@ -3808,10 +4101,10 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
3808
4101
|
|
|
3809
4102
|
// Apply the sorted order to `attrs`
|
|
3810
4103
|
const attrMap = Object.create(null);
|
|
3811
|
-
names.forEach(function (name, index) {
|
|
4104
|
+
names.forEach(function (/** @type {string} */ name, /** @type {number} */ index) {
|
|
3812
4105
|
(attrMap[name] || (attrMap[name] = [])).push(attrs[index]);
|
|
3813
4106
|
});
|
|
3814
|
-
sortedNames.forEach(function (name, index) {
|
|
4107
|
+
/** @type {string[]} */ (sortedNames).forEach(function (/** @type {string} */ name, /** @type {number} */ index) {
|
|
3815
4108
|
attrs[index] = attrMap[name].shift();
|
|
3816
4109
|
});
|
|
3817
4110
|
}
|
|
@@ -3822,14 +4115,14 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
3822
4115
|
// Memoize `sortClassNames` results—class lists often repeat in templates
|
|
3823
4116
|
const classNameCache = new LRU(500);
|
|
3824
4117
|
|
|
3825
|
-
options.sortClassNames = function (value) {
|
|
4118
|
+
options.sortClassNames = function (/** @type {string} */ value) {
|
|
3826
4119
|
// Fast path: Single class (no spaces) needs no sorting
|
|
3827
4120
|
if (value.indexOf(' ') === -1) {
|
|
3828
4121
|
return value;
|
|
3829
4122
|
}
|
|
3830
4123
|
|
|
3831
4124
|
// Check cache first
|
|
3832
|
-
const cached = classNameCache.get(value);
|
|
4125
|
+
const cached = /** @type {string | undefined} */ (classNameCache.get(value));
|
|
3833
4126
|
if (cached !== undefined) {
|
|
3834
4127
|
return cached;
|
|
3835
4128
|
}
|
|
@@ -3838,13 +4131,13 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
3838
4131
|
// Fast path: Skip if no HTML comments (UID markers) present
|
|
3839
4132
|
let expandedValue = value;
|
|
3840
4133
|
if (uidReplacePattern && value.indexOf('<!--') !== -1) {
|
|
3841
|
-
expandedValue = value.replace(uidReplacePattern, function (
|
|
4134
|
+
expandedValue = value.replace(uidReplacePattern, function (/** @type {string} */ _match, /** @type {string} */ index) {
|
|
3842
4135
|
return ignoredMarkupChunks[+index] || '';
|
|
3843
4136
|
});
|
|
3844
4137
|
// Reset `lastIndex` for pattern reuse
|
|
3845
4138
|
uidReplacePattern.lastIndex = 0;
|
|
3846
4139
|
}
|
|
3847
|
-
const classes = expandedValue.split(whitespaceSplitPatternSort).filter(function(cls) {
|
|
4140
|
+
const classes = expandedValue.split(whitespaceSplitPatternSort).filter(function(/** @type {string} */ cls) {
|
|
3848
4141
|
return cls !== '';
|
|
3849
4142
|
});
|
|
3850
4143
|
const sorted = sorter.sort(classes);
|
|
@@ -3873,29 +4166,46 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3873
4166
|
value = collapseWhitespace(value, options, true, true);
|
|
3874
4167
|
}
|
|
3875
4168
|
|
|
4169
|
+
const resolveName = /** @type {(name: string) => string} */ (options.name);
|
|
4170
|
+
/** @type {HTMLAttribute[]} */
|
|
4171
|
+
const emptyAttrs = [];
|
|
4172
|
+
|
|
4173
|
+
/** @type {string[]} */
|
|
3876
4174
|
const buffer = [];
|
|
3877
|
-
|
|
4175
|
+
/** @type {string} */
|
|
4176
|
+
let charsPrevTag = '';
|
|
3878
4177
|
let currentChars = '';
|
|
3879
|
-
|
|
4178
|
+
/** @type {boolean} */
|
|
4179
|
+
let hasChars = false;
|
|
3880
4180
|
let currentTag = '';
|
|
4181
|
+
/** @type {Array<{name: string, value?: string, quote?: string, customAssign?: string, customOpen?: string, customClose?: string}>} */
|
|
3881
4182
|
let currentAttrs = [];
|
|
4183
|
+
/** @type {string[]} */
|
|
3882
4184
|
const stackNoTrimWhitespace = [];
|
|
4185
|
+
/** @type {string[]} */
|
|
3883
4186
|
const stackNoCollapseWhitespace = [];
|
|
3884
4187
|
let preTextareaDepth = 0; // Count of `pre`/`textarea` entries in `stackNoTrimWhitespace`
|
|
3885
4188
|
let optionalStartTag = '';
|
|
3886
4189
|
let optionalEndTag = '';
|
|
3887
4190
|
let optionalEndTagEmitted = false;
|
|
4191
|
+
/** @type {string[]} */
|
|
3888
4192
|
const ignoredMarkupChunks = [];
|
|
4193
|
+
/** @type {Array<RegExpExecArray | null>} */
|
|
3889
4194
|
const ignoredCustomMarkupChunks = [];
|
|
4195
|
+
/** @type {string | undefined} */
|
|
3890
4196
|
let uidIgnore;
|
|
4197
|
+
/** @type {RegExp | undefined} */
|
|
3891
4198
|
let uidIgnorePlaceholderPattern;
|
|
4199
|
+
/** @type {string | undefined} */
|
|
3892
4200
|
let uidAttr;
|
|
4201
|
+
/** @type {RegExp | undefined} */
|
|
3893
4202
|
let uidPattern;
|
|
4203
|
+
/** @type {RegExp | undefined} */
|
|
3894
4204
|
let uidAttrLeadingPattern;
|
|
3895
4205
|
// Create inline tags/text sets with custom elements
|
|
3896
4206
|
const customElementsInput = options.inlineCustomElements ?? [];
|
|
3897
4207
|
const customElementsArr = Array.isArray(customElementsInput) ? customElementsInput : Array.from(customElementsInput);
|
|
3898
|
-
const normalizedCustomElements = customElementsArr.map(name =>
|
|
4208
|
+
const normalizedCustomElements = customElementsArr.map(name => resolveName(name));
|
|
3899
4209
|
// Fast path: Reuse base sets if no custom elements
|
|
3900
4210
|
const inlineTextSet = normalizedCustomElements.length
|
|
3901
4211
|
? new Set([...inlineElementsToKeepWhitespaceWithin, ...normalizedCustomElements])
|
|
@@ -3905,6 +4215,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3905
4215
|
: inlineElementsToKeepWhitespaceAround;
|
|
3906
4216
|
|
|
3907
4217
|
// Parse `removeEmptyElementsExcept` option
|
|
4218
|
+
/** @type {Array<{tag: string, attrs: {[x: string]: string | undefined} | null}>} */
|
|
3908
4219
|
let removeEmptyElementsExcept;
|
|
3909
4220
|
if (options.removeEmptyElementsExcept && !Array.isArray(options.removeEmptyElementsExcept)) {
|
|
3910
4221
|
if (options.log) {
|
|
@@ -3912,7 +4223,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3912
4223
|
}
|
|
3913
4224
|
removeEmptyElementsExcept = [];
|
|
3914
4225
|
} else {
|
|
3915
|
-
removeEmptyElementsExcept = parseRemoveEmptyElementsExcept(options.removeEmptyElementsExcept, options) || [];
|
|
4226
|
+
removeEmptyElementsExcept = parseRemoveEmptyElementsExcept(options.removeEmptyElementsExcept || [], /** @type {any} */ (options)) || [];
|
|
3916
4227
|
}
|
|
3917
4228
|
|
|
3918
4229
|
// Temporarily replace ignored chunks with comments, so that there’s no need to worry what’s there;
|
|
@@ -3954,17 +4265,17 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3954
4265
|
// This allows proper frequency analysis with access to ignored content via UID tokens
|
|
3955
4266
|
if ((options.sortAttributes && typeof options.sortAttributes !== 'function') ||
|
|
3956
4267
|
(options.sortClassNames && typeof options.sortClassNames !== 'function')) {
|
|
3957
|
-
await createSortFns(value, options, uidIgnore,
|
|
4268
|
+
await createSortFns(value, options, uidIgnore ?? '', uidAttr ?? '', ignoredMarkupChunks);
|
|
3958
4269
|
}
|
|
3959
4270
|
|
|
3960
|
-
const customFragments = options.ignoreCustomFragments.map(function (re) {
|
|
4271
|
+
const customFragments = (options.ignoreCustomFragments || []).map(function (/** @type {RegExp} */ re) {
|
|
3961
4272
|
return re.source;
|
|
3962
4273
|
});
|
|
3963
4274
|
if (customFragments.length) {
|
|
3964
4275
|
// Warn about potential ReDoS if custom fragments use unlimited quantifiers
|
|
3965
|
-
for (
|
|
3966
|
-
if (/[*+]/.test(
|
|
3967
|
-
options.log('Warning: Custom fragment contains unlimited quantifiers (“*” or “+”) which may cause ReDoS vulnerability');
|
|
4276
|
+
for (const fragment of customFragments) {
|
|
4277
|
+
if (/[*+]/.test(fragment)) {
|
|
4278
|
+
options.log?.('Warning: Custom fragment contains unlimited quantifiers (“*” or “+”) which may cause ReDoS vulnerability');
|
|
3968
4279
|
break;
|
|
3969
4280
|
}
|
|
3970
4281
|
}
|
|
@@ -3992,28 +4303,28 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3992
4303
|
uidPattern = new RegExp('(\\s*)' + uidAttr + '([0-9]+)' + uidAttr + '(\\s*)', 'g');
|
|
3993
4304
|
uidAttrLeadingPattern = new RegExp('^\\s*' + uidAttr + '(\\d+)' + uidAttr);
|
|
3994
4305
|
|
|
3995
|
-
if (options.minifyCSS) {
|
|
3996
|
-
options.minifyCSS = (function (fn) {
|
|
3997
|
-
return function (text, type) {
|
|
3998
|
-
text = text.replace(uidPattern, function (
|
|
4306
|
+
if (options.minifyCSS !== identity) {
|
|
4307
|
+
/** @type {any} */ (options).minifyCSS = (function (/** @type {Function} */ fn) {
|
|
4308
|
+
return function (/** @type {string} */ text, /** @type {string} */ type) {
|
|
4309
|
+
text = text.replace(/** @type {RegExp} */ (uidPattern), function (/** @type {string} */ _match, /** @type {string} */ _prefix, /** @type {string} */ index) {
|
|
3999
4310
|
const chunks = ignoredCustomMarkupChunks[+index];
|
|
4000
|
-
return chunks[1] + uidAttr + index + uidAttr + chunks[2];
|
|
4311
|
+
return (chunks?.[1] ?? '') + uidAttr + index + uidAttr + (chunks?.[2] ?? '');
|
|
4001
4312
|
});
|
|
4002
4313
|
|
|
4003
4314
|
return fn(text, type);
|
|
4004
4315
|
};
|
|
4005
|
-
})(options.minifyCSS);
|
|
4316
|
+
})(/** @type {Function} */ (options.minifyCSS));
|
|
4006
4317
|
}
|
|
4007
4318
|
|
|
4008
|
-
if (options.minifyJS) {
|
|
4009
|
-
options.minifyJS = (function (fn) {
|
|
4010
|
-
return function (text, inline, isModule) {
|
|
4011
|
-
return fn(text.replace(uidPattern, function (
|
|
4319
|
+
if (options.minifyJS !== identity) {
|
|
4320
|
+
options.minifyJS = (function (/** @type {Function} */ fn) {
|
|
4321
|
+
return function (/** @type {string} */ text, /** @type {boolean} */ inline, /** @type {boolean} */ isModule) {
|
|
4322
|
+
return fn(text.replace(/** @type {RegExp} */ (uidPattern), function (/** @type {string} */ _match, /** @type {string} */ _prefix, /** @type {string} */ index) {
|
|
4012
4323
|
const chunks = ignoredCustomMarkupChunks[+index];
|
|
4013
|
-
return chunks[1] + uidAttr + index + uidAttr + chunks[2];
|
|
4324
|
+
return (chunks?.[1] ?? '') + uidAttr + index + uidAttr + (chunks?.[2] ?? '');
|
|
4014
4325
|
}), inline, isModule);
|
|
4015
4326
|
};
|
|
4016
|
-
})(options.minifyJS);
|
|
4327
|
+
})(/** @type {Function} */ (options.minifyJS));
|
|
4017
4328
|
}
|
|
4018
4329
|
}
|
|
4019
4330
|
|
|
@@ -4024,17 +4335,17 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4024
4335
|
}
|
|
4025
4336
|
}
|
|
4026
4337
|
|
|
4027
|
-
function canCollapseWhitespace$1(tag, attrs) {
|
|
4028
|
-
return options.canCollapseWhitespace(tag, attrs, canCollapseWhitespace);
|
|
4338
|
+
function canCollapseWhitespace$1(/** @type {string} */ tag, /** @type {HTMLAttribute[]} */ attrs) {
|
|
4339
|
+
return /** @type {Function} */ (options.canCollapseWhitespace)(tag, attrs, canCollapseWhitespace);
|
|
4029
4340
|
}
|
|
4030
4341
|
|
|
4031
|
-
function canTrimWhitespace$1(tag, attrs) {
|
|
4032
|
-
return options.canTrimWhitespace(tag, attrs, canTrimWhitespace);
|
|
4342
|
+
function canTrimWhitespace$1(/** @type {string} */ tag, /** @type {HTMLAttribute[]} */ attrs) {
|
|
4343
|
+
return /** @type {Function} */ (options.canTrimWhitespace)(tag, attrs, canTrimWhitespace);
|
|
4033
4344
|
}
|
|
4034
4345
|
|
|
4035
4346
|
function removeStartTag() {
|
|
4036
4347
|
let index = buffer.length - 1;
|
|
4037
|
-
while (index > 0 && !RE_START_TAG.test(buffer[index])) {
|
|
4348
|
+
while (index > 0 && !RE_START_TAG.test(buffer[index] ?? '')) {
|
|
4038
4349
|
index--;
|
|
4039
4350
|
}
|
|
4040
4351
|
buffer.length = Math.max(0, index);
|
|
@@ -4042,20 +4353,20 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4042
4353
|
|
|
4043
4354
|
function removeEndTag() {
|
|
4044
4355
|
let index = buffer.length - 1;
|
|
4045
|
-
while (index > 0 && !RE_END_TAG.test(buffer[index])) {
|
|
4356
|
+
while (index > 0 && !RE_END_TAG.test(buffer[index] ?? '')) {
|
|
4046
4357
|
index--;
|
|
4047
4358
|
}
|
|
4048
4359
|
buffer.length = Math.max(0, index);
|
|
4049
4360
|
}
|
|
4050
4361
|
|
|
4051
4362
|
// Look for trailing whitespaces, bypass any inline tags
|
|
4052
|
-
function trimTrailingWhitespace(index, nextTag) {
|
|
4053
|
-
for (let endTag =
|
|
4054
|
-
const str = buffer[index];
|
|
4363
|
+
function trimTrailingWhitespace(/** @type {number} */ index, /** @type {string} */ nextTag) {
|
|
4364
|
+
for (let endTag = ''; index >= 0 && canTrimWhitespace$1(endTag, emptyAttrs); index--) {
|
|
4365
|
+
const str = buffer[index] ?? '';
|
|
4055
4366
|
const match = str.match(/^<\/([\w:-]+)>$/);
|
|
4056
4367
|
if (match) {
|
|
4057
|
-
endTag = match[1];
|
|
4058
|
-
} else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str,
|
|
4368
|
+
endTag = match[1] ?? '';
|
|
4369
|
+
} else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, '', nextTag, emptyAttrs, emptyAttrs, options, inlineElements, inlineTextSet))) {
|
|
4059
4370
|
break;
|
|
4060
4371
|
}
|
|
4061
4372
|
}
|
|
@@ -4064,10 +4375,10 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4064
4375
|
// Look for trailing whitespaces from previously processed text
|
|
4065
4376
|
// which may not be trimmed due to a following comment or an empty
|
|
4066
4377
|
// element which has now been removed
|
|
4067
|
-
function squashTrailingWhitespace(nextTag) {
|
|
4378
|
+
function squashTrailingWhitespace(/** @type {string} */ nextTag) {
|
|
4068
4379
|
let charsIndex = buffer.length - 1;
|
|
4069
4380
|
if (buffer.length > 1) {
|
|
4070
|
-
const item = buffer[buffer.length - 1];
|
|
4381
|
+
const item = buffer[buffer.length - 1] ?? '';
|
|
4071
4382
|
if (/^(?:<!|$)/.test(item) && (!uidIgnore || item.indexOf(uidIgnore) === -1)) {
|
|
4072
4383
|
charsIndex--;
|
|
4073
4384
|
}
|
|
@@ -4076,6 +4387,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4076
4387
|
}
|
|
4077
4388
|
|
|
4078
4389
|
// SVG subtree capture: When SVGO is active, record buffer positions for post-processing
|
|
4390
|
+
/** @type {Array<{start: number, end: number}>} */
|
|
4079
4391
|
const svgBlocks = []; // Array of { start, end } buffer indices
|
|
4080
4392
|
let svgBufferStartIndex = -1;
|
|
4081
4393
|
let svgDepth = 0;
|
|
@@ -4088,7 +4400,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4088
4400
|
// Compute `nextTag` only when whitespace collapse features require it
|
|
4089
4401
|
wantsNextTag: !!(options.collapseWhitespace || options.collapseInlineTagWhitespace || options.conservativeCollapse),
|
|
4090
4402
|
|
|
4091
|
-
start: async function (tag, attrs, unary, unarySlash, autoGenerated) {
|
|
4403
|
+
start: async function (/** @type {string} */ tag, /** @type {HTMLAttribute[]} */ attrs, /** @type {boolean} */ unary, /** @type {string} */ unarySlash, /** @type {boolean} */ autoGenerated) {
|
|
4092
4404
|
const lowerTag = tag.toLowerCase();
|
|
4093
4405
|
if (lowerTag === 'svg' || lowerTag === 'math') {
|
|
4094
4406
|
options = Object.create(options);
|
|
@@ -4110,21 +4422,21 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4110
4422
|
// Note: The element itself is in SVG/MathML namespace, only its children are HTML
|
|
4111
4423
|
let useParentNameForTag = false;
|
|
4112
4424
|
if (options.insideForeignContent && (lowerTag === 'foreignobject' ||
|
|
4113
|
-
(lowerTag === 'annotation-xml' && attrs.some(a => a.name.toLowerCase() === 'encoding' &&
|
|
4114
|
-
RE_HTML_ENCODING.test(a.value))))) {
|
|
4115
|
-
const parentName = options.name;
|
|
4425
|
+
(lowerTag === 'annotation-xml' && attrs.some((/** @type {HTMLAttribute} */ a) => a.name.toLowerCase() === 'encoding' &&
|
|
4426
|
+
RE_HTML_ENCODING.test(a.value ?? ''))))) {
|
|
4427
|
+
const parentName = /** @type {(name: string) => string} */ (options.name);
|
|
4116
4428
|
options = Object.create(options);
|
|
4117
4429
|
options.caseSensitive = false;
|
|
4118
4430
|
options.keepClosingSlash = false;
|
|
4119
4431
|
options.parentName = parentName; // Preserve for the element tag itself
|
|
4120
|
-
options.name = options.htmlName
|
|
4432
|
+
options.name = /** @type {(name: string) => string} */ (options.htmlName ?? lowercase);
|
|
4121
4433
|
options.insideForeignContent = false;
|
|
4122
4434
|
// Note: `removeAttributeQuotes`, `removeTagWhitespace`, and `decodeEntities`
|
|
4123
4435
|
// stay disabled (inherited from SVG context) because the entire SVG block
|
|
4124
4436
|
// must be valid XML for SVGO processing
|
|
4125
4437
|
useParentNameForTag = true;
|
|
4126
4438
|
}
|
|
4127
|
-
tag = (useParentNameForTag ? options.parentName : options.name)(tag);
|
|
4439
|
+
tag = /** @type {(name: string) => string} */ (useParentNameForTag ? options.parentName : options.name)(tag);
|
|
4128
4440
|
currentTag = tag;
|
|
4129
4441
|
charsPrevTag = tag;
|
|
4130
4442
|
if (!inlineTextSet.has(tag)) {
|
|
@@ -4190,10 +4502,10 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4190
4502
|
// Remove duplicate attributes (per HTML spec, first occurrence wins)
|
|
4191
4503
|
// Duplicate attributes result in invalid HTML
|
|
4192
4504
|
// https://html.spec.whatwg.org/multipage/parsing.html#attribute-name-state
|
|
4193
|
-
deduplicateAttributes(attrs, options.caseSensitive);
|
|
4505
|
+
deduplicateAttributes(attrs, Boolean(options.caseSensitive));
|
|
4194
4506
|
|
|
4195
4507
|
if (options.sortAttributes) {
|
|
4196
|
-
options.sortAttributes(tag, attrs);
|
|
4508
|
+
/** @type {Function} */ (options.sortAttributes)(tag, attrs);
|
|
4197
4509
|
}
|
|
4198
4510
|
|
|
4199
4511
|
const attrResults = attrs.map(attr => normalizeAttr(attr, attrs, tag, options, minifyHTML));
|
|
@@ -4223,7 +4535,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4223
4535
|
currentTag = '';
|
|
4224
4536
|
}
|
|
4225
4537
|
},
|
|
4226
|
-
end: function (tag, attrs, autoGenerated) {
|
|
4538
|
+
end: function (/** @type {string} */ tag, /** @type {HTMLAttribute[]} */ attrs, /** @type {boolean} */ autoGenerated) {
|
|
4227
4539
|
const lowerTag = tag.toLowerCase();
|
|
4228
4540
|
// Restore parent context when exiting SVG/MathML or HTML-in-foreign-content elements
|
|
4229
4541
|
if (lowerTag === 'svg' || lowerTag === 'math') {
|
|
@@ -4232,7 +4544,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4232
4544
|
!options.insideForeignContent && Object.getPrototypeOf(options).insideForeignContent) {
|
|
4233
4545
|
options = Object.getPrototypeOf(options);
|
|
4234
4546
|
}
|
|
4235
|
-
tag = options.name(tag);
|
|
4547
|
+
tag = /** @type {Function} */ (options.name)(tag);
|
|
4236
4548
|
|
|
4237
4549
|
// Check if current tag is in a whitespace stack
|
|
4238
4550
|
if (options.collapseWhitespace) {
|
|
@@ -4278,7 +4590,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4278
4590
|
let preserve = false;
|
|
4279
4591
|
if (removeEmptyElementsExcept.length) {
|
|
4280
4592
|
// Normalize attribute names for comparison with specs
|
|
4281
|
-
const normalizedAttrs = attrs.map(attr => ({ ...attr, name:
|
|
4593
|
+
const normalizedAttrs = attrs.map((/** @type {HTMLAttribute} */ attr) => ({ ...attr, name: resolveName(attr.name) }));
|
|
4282
4594
|
preserve = shouldPreserveEmptyElement(tag, normalizedAttrs, removeEmptyElementsExcept);
|
|
4283
4595
|
}
|
|
4284
4596
|
|
|
@@ -4325,7 +4637,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4325
4637
|
}
|
|
4326
4638
|
}
|
|
4327
4639
|
},
|
|
4328
|
-
chars: function (text, prevTag, nextTag, prevAttrs, nextAttrs) {
|
|
4640
|
+
chars: function (/** @type {string} */ text, /** @type {string} */ prevTag, /** @type {string} */ nextTag, /** @type {HTMLAttribute[]} */ prevAttrs, /** @type {HTMLAttribute[]} */ nextAttrs) {
|
|
4329
4641
|
prevTag = prevTag === '' ? 'comment' : prevTag;
|
|
4330
4642
|
nextTag = nextTag === '' ? 'comment' : nextTag;
|
|
4331
4643
|
prevAttrs = prevAttrs || [];
|
|
@@ -4341,7 +4653,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4341
4653
|
const needsMinifyCSS = options.minifyCSS !== identity && isStyleElement(currentTag, currentAttrs);
|
|
4342
4654
|
|
|
4343
4655
|
// Whitespace collapsing phase (sync); captures `prevTag`/`nextTag`/`prevAttrs`/`nextAttrs` from outer scope
|
|
4344
|
-
function charsCollapse(text) {
|
|
4656
|
+
function charsCollapse(/** @type {string} */ text) {
|
|
4345
4657
|
// Trim outermost newline-based whitespace inside `pre`/`textarea` elements
|
|
4346
4658
|
// This removes trailing newlines often added by template engines before closing tags
|
|
4347
4659
|
// Only trims single trailing newlines (multiple newlines are likely intentional formatting)
|
|
@@ -4363,7 +4675,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4363
4675
|
// to avoid side effects on the `inlineTextSet` branch below
|
|
4364
4676
|
let effectivePrevTag = prevTag;
|
|
4365
4677
|
if (prevTag === 'comment') {
|
|
4366
|
-
const prevComment = buffer[buffer.length - 1];
|
|
4678
|
+
const prevComment = buffer[buffer.length - 1] ?? '';
|
|
4367
4679
|
if (!uidIgnore || prevComment.indexOf(uidIgnore) === -1) {
|
|
4368
4680
|
if (!prevComment) {
|
|
4369
4681
|
prevTag = charsPrevTag;
|
|
@@ -4371,7 +4683,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4371
4683
|
}
|
|
4372
4684
|
if (buffer.length > 1 && (!prevComment || (!options.conservativeCollapse && / $/.test(currentChars)))) {
|
|
4373
4685
|
const charsIndex = buffer.length - 2;
|
|
4374
|
-
buffer[charsIndex] = buffer[charsIndex].replace(/\s+$/, function (trailingSpaces) {
|
|
4686
|
+
buffer[charsIndex] = (buffer[charsIndex] ?? '').replace(/\s+$/, function (trailingSpaces) {
|
|
4375
4687
|
text = trailingSpaces + text;
|
|
4376
4688
|
return '';
|
|
4377
4689
|
});
|
|
@@ -4382,13 +4694,14 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4382
4694
|
// when `nextTag` is `comment` (another UID placeholder), `commentFinalize` handles it
|
|
4383
4695
|
const match = prevComment.match(uidIgnorePlaceholderPattern);
|
|
4384
4696
|
if (match) {
|
|
4385
|
-
const idx = +match[1];
|
|
4697
|
+
const idx = +(match[1] ?? '');
|
|
4386
4698
|
if (idx < ignoredMarkupChunks.length) {
|
|
4387
4699
|
const content = ignoredMarkupChunks[idx];
|
|
4388
4700
|
const lastTagMatch = content && RE_LAST_HTML_TAG.exec(content);
|
|
4389
4701
|
if (lastTagMatch) {
|
|
4390
|
-
const
|
|
4391
|
-
const
|
|
4702
|
+
const group = lastTagMatch[1] ?? '';
|
|
4703
|
+
const isClose = group.charAt(0) === '/';
|
|
4704
|
+
const tagName = resolveName(isClose ? group.slice(1) : group);
|
|
4392
4705
|
effectivePrevTag = isClose ? '/' + tagName : tagName;
|
|
4393
4706
|
}
|
|
4394
4707
|
}
|
|
@@ -4399,13 +4712,13 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4399
4712
|
if (prevTag === '/nobr' || prevTag === 'wbr') {
|
|
4400
4713
|
if (/^\s/.test(text)) {
|
|
4401
4714
|
let tagIndex = buffer.length - 1;
|
|
4402
|
-
while (tagIndex > 0 && buffer[tagIndex].lastIndexOf('<' + prevTag) !== 0) {
|
|
4715
|
+
while (tagIndex > 0 && (buffer[tagIndex] ?? '').lastIndexOf('<' + prevTag) !== 0) {
|
|
4403
4716
|
tagIndex--;
|
|
4404
4717
|
}
|
|
4405
4718
|
trimTrailingWhitespace(tagIndex - 1, 'br');
|
|
4406
4719
|
}
|
|
4407
4720
|
} else if (inlineTextSet.has(prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag)) {
|
|
4408
|
-
text = collapseWhitespace(text, options, /(?:^|\s)$/.test(currentChars));
|
|
4721
|
+
text = collapseWhitespace(text, options, /(?:^|\s)$/.test(currentChars), false);
|
|
4409
4722
|
}
|
|
4410
4723
|
}
|
|
4411
4724
|
if (prevTag || nextTag) {
|
|
@@ -4425,18 +4738,18 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4425
4738
|
}
|
|
4426
4739
|
|
|
4427
4740
|
// Finalization phase (sync): Optional tag handling, entity re-encoding, buffer push
|
|
4428
|
-
function charsFinalize(text) {
|
|
4741
|
+
function charsFinalize(/** @type {string} */ text) {
|
|
4429
4742
|
if (options.removeOptionalTags && text) {
|
|
4430
4743
|
// UID-attr tokens are padded with `\t`, which would falsely look like leading whitespace;
|
|
4431
4744
|
// resolve single-token text to its actual content for the space/comment checks below
|
|
4432
4745
|
let effectiveText = text;
|
|
4433
|
-
if (uidAttrLeadingPattern && text.includes(uidAttr)) {
|
|
4746
|
+
if (uidAttrLeadingPattern && uidAttr && text.includes(uidAttr)) {
|
|
4434
4747
|
const uidMatch = uidAttrLeadingPattern.exec(text);
|
|
4435
4748
|
if (uidMatch) {
|
|
4436
|
-
const idx = +uidMatch[1];
|
|
4749
|
+
const idx = +(uidMatch[1] ?? 0);
|
|
4437
4750
|
const chunks = idx < ignoredCustomMarkupChunks.length ? ignoredCustomMarkupChunks[idx] : null;
|
|
4438
4751
|
if (chunks != null) {
|
|
4439
|
-
effectiveText = chunks[0];
|
|
4752
|
+
effectiveText = chunks[0] ?? '';
|
|
4440
4753
|
}
|
|
4441
4754
|
}
|
|
4442
4755
|
}
|
|
@@ -4472,8 +4785,8 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4472
4785
|
}
|
|
4473
4786
|
}
|
|
4474
4787
|
if (uidPattern && options.collapseWhitespace && stackNoTrimWhitespace.length) {
|
|
4475
|
-
text = text.replace(uidPattern, function (match,
|
|
4476
|
-
return ignoredCustomMarkupChunks[+index][0];
|
|
4788
|
+
text = text.replace(/** @type {RegExp} */ (uidPattern), function (/** @type {string} */ match, /** @type {string} */ _prefix, /** @type {string} */ index) {
|
|
4789
|
+
return ignoredCustomMarkupChunks[+index]?.[0] ?? match;
|
|
4477
4790
|
});
|
|
4478
4791
|
}
|
|
4479
4792
|
currentChars += text;
|
|
@@ -4499,20 +4812,20 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4499
4812
|
text = await processScript(text, options, currentAttrs, minifyHTML);
|
|
4500
4813
|
}
|
|
4501
4814
|
if (needsMinifyJS) {
|
|
4502
|
-
text = await options.minifyJS(text, false, isModuleScript);
|
|
4815
|
+
text = await /** @type {Function} */ (options.minifyJS)(text, false, isModuleScript);
|
|
4503
4816
|
}
|
|
4504
4817
|
if (needsMinifyCSS) {
|
|
4505
|
-
text = await options.minifyCSS(text);
|
|
4818
|
+
text = await /** @type {Function} */ (options.minifyCSS)(text);
|
|
4506
4819
|
}
|
|
4507
4820
|
charsFinalize(text);
|
|
4508
4821
|
})();
|
|
4509
4822
|
},
|
|
4510
|
-
comment: function (text, nonStandard) {
|
|
4823
|
+
comment: function (/** @type {string} */ text, /** @type {boolean} */ nonStandard) {
|
|
4511
4824
|
const prefix = nonStandard ? '<!' : '<!--';
|
|
4512
4825
|
const suffix = nonStandard ? '>' : '-->';
|
|
4513
4826
|
|
|
4514
4827
|
// Finalization phase (sync): Optional tag handling, `htmlmin:ignore` whitespace collapsing, buffer push
|
|
4515
|
-
function commentFinalize(comment) {
|
|
4828
|
+
function commentFinalize(/** @type {string} */ comment) {
|
|
4516
4829
|
if (options.removeOptionalTags && comment) {
|
|
4517
4830
|
if (uidIgnorePlaceholderPattern) {
|
|
4518
4831
|
const match = uidIgnorePlaceholderPattern.exec(comment);
|
|
@@ -4521,11 +4834,12 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4521
4834
|
// if there’s a pending optional end tag and the ignored content isn’t itself
|
|
4522
4835
|
// a comment (which per the HTML spec prevents omission), resolve it now,
|
|
4523
4836
|
// before the UID is pushed to the buffer
|
|
4524
|
-
const idx = +match[1];
|
|
4837
|
+
const idx = +(match[1] ?? 0);
|
|
4525
4838
|
const content = idx < ignoredMarkupChunks.length ? ignoredMarkupChunks[idx] : null;
|
|
4526
4839
|
if (optionalEndTag && optionalEndTagEmitted && content != null && !/^\s*<!--/.test(content)) {
|
|
4527
4840
|
const firstTagMatch = content.match(/^\s*<([a-zA-Z][^\s/>]*)/);
|
|
4528
|
-
const
|
|
4841
|
+
const firstTagGroup = firstTagMatch?.[1] ?? '';
|
|
4842
|
+
const firstTag = firstTagGroup ? resolveName(firstTagGroup) : '';
|
|
4529
4843
|
if (canRemovePrecedingTag(optionalEndTag, firstTag)) {
|
|
4530
4844
|
removeEndTag();
|
|
4531
4845
|
}
|
|
@@ -4553,8 +4867,8 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4553
4867
|
const prevMatch = prevComment.match(uidIgnorePlaceholderPattern);
|
|
4554
4868
|
|
|
4555
4869
|
if (currentMatch && prevMatch) {
|
|
4556
|
-
const currentIndex = +currentMatch[1];
|
|
4557
|
-
const prevIndex = +prevMatch[1];
|
|
4870
|
+
const currentIndex = +(currentMatch[1] ?? 0);
|
|
4871
|
+
const prevIndex = +(prevMatch[1] ?? 0);
|
|
4558
4872
|
|
|
4559
4873
|
// Defensive bounds check to ensure indices are valid
|
|
4560
4874
|
if (currentIndex < ignoredMarkupChunks.length && prevIndex < ignoredMarkupChunks.length) {
|
|
@@ -4577,13 +4891,13 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4577
4891
|
// Collapse if both sides are element/closing tags or HTML comments, and neither is inline
|
|
4578
4892
|
if ((currentTagMatch || currentIsHtmlComment || currentClosingTagMatch) &&
|
|
4579
4893
|
(prevTagMatch || prevIsHtmlComment || prevClosingTagMatch)) {
|
|
4580
|
-
const currentTag = currentTagMatch ?
|
|
4581
|
-
: currentClosingTagMatch ?
|
|
4582
|
-
const prevTag = prevTagMatch ?
|
|
4583
|
-
: prevClosingTagMatch ?
|
|
4894
|
+
const currentTag = currentTagMatch ? resolveName(currentTagMatch[1] ?? '')
|
|
4895
|
+
: currentClosingTagMatch ? resolveName(currentClosingTagMatch[1] ?? '') : null;
|
|
4896
|
+
const prevTag = prevTagMatch ? resolveName(prevTagMatch[1] ?? '')
|
|
4897
|
+
: prevClosingTagMatch ? resolveName(prevClosingTagMatch[1] ?? '') : null;
|
|
4584
4898
|
|
|
4585
4899
|
// Don’t collapse between inline elements (HTML comments count as non-inline)
|
|
4586
|
-
if (!inlineElements.has(currentTag) && !inlineElements.has(prevTag)) {
|
|
4900
|
+
if (!inlineElements.has(currentTag ?? '') && !inlineElements.has(prevTag ?? '')) {
|
|
4587
4901
|
// Collapse whitespace respecting context rules
|
|
4588
4902
|
let collapsedText = prevText;
|
|
4589
4903
|
|
|
@@ -4618,7 +4932,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4618
4932
|
}
|
|
4619
4933
|
|
|
4620
4934
|
if (options.removeComments) {
|
|
4621
|
-
if (isIgnoredComment(text, options)) {
|
|
4935
|
+
if (isIgnoredComment(text, /** @type {any} */ (options))) {
|
|
4622
4936
|
text = prefix + text + suffix;
|
|
4623
4937
|
} else {
|
|
4624
4938
|
text = '';
|
|
@@ -4628,7 +4942,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4628
4942
|
}
|
|
4629
4943
|
commentFinalize(text);
|
|
4630
4944
|
},
|
|
4631
|
-
doctype: function (doctype) {
|
|
4945
|
+
doctype: function (/** @type {string} */ doctype) {
|
|
4632
4946
|
buffer.push(options.useShortDoctype
|
|
4633
4947
|
? '<!doctype' +
|
|
4634
4948
|
(options.removeTagWhitespace ? '' : ' ') + 'html>'
|
|
@@ -4643,11 +4957,12 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4643
4957
|
if (options.minifySVG && svgBlocks.length) {
|
|
4644
4958
|
const optimized = await Promise.all(
|
|
4645
4959
|
svgBlocks.map(({ start, end }) =>
|
|
4646
|
-
options.minifySVG(buffer.slice(start, end).join(''))
|
|
4960
|
+
/** @type {Function} */ (options.minifySVG)(buffer.slice(start, end).join(''))
|
|
4647
4961
|
)
|
|
4648
4962
|
);
|
|
4649
4963
|
for (let i = svgBlocks.length - 1; i >= 0; i--) {
|
|
4650
|
-
|
|
4964
|
+
const block = svgBlocks[i];
|
|
4965
|
+
if (block) buffer.splice(block.start, block.end - block.start, optimized[i] ?? '');
|
|
4651
4966
|
}
|
|
4652
4967
|
}
|
|
4653
4968
|
|
|
@@ -4667,9 +4982,9 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4667
4982
|
}
|
|
4668
4983
|
|
|
4669
4984
|
return joinResultSegments(buffer, options, uidPattern
|
|
4670
|
-
? function (str) {
|
|
4671
|
-
return str.replace(uidPattern, function (match, prefix, index, suffix) {
|
|
4672
|
-
let chunk = ignoredCustomMarkupChunks[+index][0];
|
|
4985
|
+
? function (/** @type {string} */ str) {
|
|
4986
|
+
return str.replace(/** @type {RegExp} */ (uidPattern), function (/** @type {string} */ match, /** @type {string} */ prefix, /** @type {string} */ index, /** @type {string} */ suffix) {
|
|
4987
|
+
let chunk = ignoredCustomMarkupChunks[+index]?.[0] ?? match;
|
|
4673
4988
|
if (options.collapseWhitespace) {
|
|
4674
4989
|
if (prefix !== '\t') {
|
|
4675
4990
|
chunk = prefix + chunk;
|
|
@@ -4686,14 +5001,20 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4686
5001
|
});
|
|
4687
5002
|
}
|
|
4688
5003
|
: identity, uidIgnore
|
|
4689
|
-
? function (str) {
|
|
4690
|
-
return str.replace(new RegExp('<!--' + uidIgnore + '([0-9]+)-->', 'g'), function (
|
|
4691
|
-
return ignoredMarkupChunks[+index];
|
|
5004
|
+
? function (/** @type {string} */ str) {
|
|
5005
|
+
return str.replace(new RegExp('<!--' + uidIgnore + '([0-9]+)-->', 'g'), function (/** @type {string} */ _match, /** @type {string} */ index) {
|
|
5006
|
+
return ignoredMarkupChunks[+index] ?? '';
|
|
4692
5007
|
});
|
|
4693
5008
|
}
|
|
4694
5009
|
: identity);
|
|
4695
5010
|
}
|
|
4696
5011
|
|
|
5012
|
+
/**
|
|
5013
|
+
* @param {string[]} results
|
|
5014
|
+
* @param {MinifierOptions} options
|
|
5015
|
+
* @param {Function} restoreCustom
|
|
5016
|
+
* @param {Function} restoreIgnore
|
|
5017
|
+
*/
|
|
4697
5018
|
function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
|
|
4698
5019
|
let str;
|
|
4699
5020
|
const maxLineLength = options.maxLineLength;
|
|
@@ -4703,15 +5024,16 @@ function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
|
|
|
4703
5024
|
let line = ''; const lines = [];
|
|
4704
5025
|
while (results.length) {
|
|
4705
5026
|
const len = line.length;
|
|
4706
|
-
const
|
|
4707
|
-
const
|
|
5027
|
+
const cur = results[0] ?? '';
|
|
5028
|
+
const end = cur.indexOf('\n');
|
|
5029
|
+
const isClosingTag = Boolean(cur.match(endTag));
|
|
4708
5030
|
const shouldKeepSameLine = noNewlinesBeforeTagClose && isClosingTag;
|
|
4709
5031
|
|
|
4710
5032
|
if (end < 0) {
|
|
4711
|
-
line += restoreIgnore(restoreCustom(results.shift()));
|
|
5033
|
+
line += restoreIgnore(restoreCustom(results.shift() ?? ''));
|
|
4712
5034
|
} else {
|
|
4713
|
-
line += restoreIgnore(restoreCustom(
|
|
4714
|
-
results[0] =
|
|
5035
|
+
line += restoreIgnore(restoreCustom(cur.slice(0, end)));
|
|
5036
|
+
results[0] = cur.slice(end + 1);
|
|
4715
5037
|
}
|
|
4716
5038
|
if (len > 0 && line.length > maxLineLength && !shouldKeepSameLine) {
|
|
4717
5039
|
lines.push(line.slice(0, len));
|
|
@@ -4741,13 +5063,14 @@ function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
|
|
|
4741
5063
|
* - The first call’s options determine the cache sizes for subsequent calls
|
|
4742
5064
|
* - Invalid values (NaN, Infinity) fall back to the default size (500); values below `1` are clamped to `1`
|
|
4743
5065
|
*/
|
|
5066
|
+
/** @param {MinifierOptions} options */
|
|
4744
5067
|
function initCaches(options) {
|
|
4745
5068
|
// Only create caches once (on first call)—sizes are locked after this
|
|
4746
5069
|
if (!cssMinifyCache) {
|
|
4747
5070
|
const defaultSize = 500;
|
|
4748
5071
|
|
|
4749
5072
|
// Helper to parse env var—returns parsed number (including 0) or undefined if absent, invalid, or negative
|
|
4750
|
-
const parseEnvCacheSize = (envVar) => {
|
|
5073
|
+
const parseEnvCacheSize = (/** @type {string | undefined} */ envVar) => {
|
|
4751
5074
|
if (envVar === undefined) return undefined;
|
|
4752
5075
|
const parsed = Number(envVar);
|
|
4753
5076
|
if (Number.isNaN(parsed) || !Number.isFinite(parsed) || parsed < 0) {
|
|
@@ -4757,7 +5080,7 @@ function initCaches(options) {
|
|
|
4757
5080
|
};
|
|
4758
5081
|
|
|
4759
5082
|
// Sanitize a cache size: Non-finite/NaN falls back to `defaultSize`; otherwise clamped to min 1 and floored
|
|
4760
|
-
const sanitizeSize = (size) => Number.isFinite(size) ? Math.max(1, Math.floor(size)) : defaultSize;
|
|
5083
|
+
const sanitizeSize = (/** @type {number} */ size) => Number.isFinite(size) ? Math.max(1, Math.floor(size)) : defaultSize;
|
|
4761
5084
|
|
|
4762
5085
|
// Get cache sizes with precedence: Options > env > default
|
|
4763
5086
|
const cssSize = options.cacheCSS !== undefined ? options.cacheCSS
|
|
@@ -4795,7 +5118,9 @@ const minify = async function (value, options) {
|
|
|
4795
5118
|
getTerser,
|
|
4796
5119
|
getSwc,
|
|
4797
5120
|
getSvgo,
|
|
4798
|
-
|
|
5121
|
+
cssMinifyCache: caches.cssMinifyCache ?? undefined,
|
|
5122
|
+
jsMinifyCache: caches.jsMinifyCache ?? undefined,
|
|
5123
|
+
svgMinifyCache: caches.svgMinifyCache ?? undefined
|
|
4799
5124
|
});
|
|
4800
5125
|
let result = await minifyHTML(value, options);
|
|
4801
5126
|
|
|
@@ -4804,7 +5129,7 @@ const minify = async function (value, options) {
|
|
|
4804
5129
|
result = mergeConsecutiveScripts(result);
|
|
4805
5130
|
}
|
|
4806
5131
|
|
|
4807
|
-
options.log('minified in: ' + (Date.now() - start) + 'ms');
|
|
5132
|
+
/** @type {Function} */ (options.log)('minified in: ' + (Date.now() - start) + 'ms');
|
|
4808
5133
|
return result;
|
|
4809
5134
|
};
|
|
4810
5135
|
|