html-minifier-next 6.2.8 → 6.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -1
- package/dist/htmlminifier.cjs +882 -557
- package/dist/types/htmlminifier.d.ts +77 -60
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/htmlparser.d.ts +19 -3
- package/dist/types/htmlparser.d.ts.map +1 -1
- package/dist/types/lib/attributes.d.ts +137 -23
- package/dist/types/lib/attributes.d.ts.map +1 -1
- package/dist/types/lib/content.d.ts +37 -5
- package/dist/types/lib/content.d.ts.map +1 -1
- package/dist/types/lib/elements.d.ts +29 -4
- package/dist/types/lib/elements.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts +17 -14
- package/dist/types/lib/options.d.ts.map +1 -1
- package/dist/types/lib/utils.d.ts +28 -11
- package/dist/types/lib/utils.d.ts.map +1 -1
- package/dist/types/lib/whitespace.d.ts +40 -6
- package/dist/types/lib/whitespace.d.ts.map +1 -1
- package/dist/types/tokenchain.d.ts +17 -3
- package/dist/types/tokenchain.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/htmlminifier.js +405 -346
- package/src/htmlparser.js +101 -48
- package/src/lib/attributes.js +166 -50
- package/src/lib/content.js +28 -12
- package/src/lib/elements.js +30 -7
- package/src/lib/options.js +55 -42
- package/src/lib/utils.js +26 -7
- package/src/lib/whitespace.js +30 -11
- package/src/presets.js +1 -1
- package/src/tokenchain.js +30 -17
package/src/htmlminifier.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// Imports
|
|
2
|
-
|
|
3
1
|
import { HTMLParser, endTag } from './htmlparser.js';
|
|
4
2
|
import TokenChain from './tokenchain.js';
|
|
5
3
|
import { presets, getPreset, getPresetNames } from './presets.js';
|
|
@@ -56,241 +54,12 @@ import {
|
|
|
56
54
|
|
|
57
55
|
import { processOptions } from './lib/options.js';
|
|
58
56
|
|
|
59
|
-
// Lazy-load heavy dependencies only when needed
|
|
60
|
-
|
|
61
|
-
let lightningCSSPromise;
|
|
62
|
-
async function getLightningCSS() {
|
|
63
|
-
if (!lightningCSSPromise) {
|
|
64
|
-
lightningCSSPromise = import('lightningcss').then(m => m.transform);
|
|
65
|
-
}
|
|
66
|
-
return lightningCSSPromise;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
let terserPromise;
|
|
70
|
-
async function getTerser() {
|
|
71
|
-
if (!terserPromise) {
|
|
72
|
-
terserPromise = import('terser').then(m => m.minify);
|
|
73
|
-
}
|
|
74
|
-
return terserPromise;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
let swcPromise;
|
|
78
|
-
async function getSwc() {
|
|
79
|
-
if (!swcPromise) {
|
|
80
|
-
swcPromise = import('@swc/core')
|
|
81
|
-
.then(m => m.default || m)
|
|
82
|
-
.catch(() => {
|
|
83
|
-
throw new Error(
|
|
84
|
-
'The swc minifier requires @swc/core to be installed.\n' +
|
|
85
|
-
'Install it with: npm install @swc/core'
|
|
86
|
-
);
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
return swcPromise;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
let svgoPromise;
|
|
93
|
-
async function getSvgo() {
|
|
94
|
-
if (!svgoPromise) {
|
|
95
|
-
svgoPromise = import('svgo').then(m => m.optimize);
|
|
96
|
-
}
|
|
97
|
-
return svgoPromise;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
let decodeHTMLPromise;
|
|
101
|
-
async function getDecodeHTML() {
|
|
102
|
-
if (!decodeHTMLPromise) {
|
|
103
|
-
decodeHTMLPromise = import('entities').then(m => m.decodeHTML);
|
|
104
|
-
}
|
|
105
|
-
return decodeHTMLPromise;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Minification caches (initialized on first use with configurable sizes)
|
|
109
|
-
let cssMinifyCache = null;
|
|
110
|
-
let jsMinifyCache = null;
|
|
111
|
-
let svgMinifyCache = null;
|
|
112
|
-
|
|
113
|
-
// Pre-compiled patterns for script merging (avoid repeated allocation in hot path)
|
|
114
|
-
const RE_SCRIPT_ATTRS = /([^\s=]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
115
|
-
const RE_SCRIPT_OPEN = /<script(?=[\s>])/gi; // Finds tag start; use `findTagEnd()` for the actual closing `>`
|
|
116
|
-
const RE_SCRIPT_CLOSE = /<\/script\s*>/gi;
|
|
117
|
-
const SCRIPT_BOOL_ATTRS = new Set(['async', 'defer', 'nomodule']);
|
|
118
|
-
const DEFAULT_JS_TYPES = new Set(['', 'text/javascript', 'application/javascript']);
|
|
119
|
-
|
|
120
|
-
// Pre-compiled patterns for buffer scanning
|
|
121
|
-
const RE_START_TAG = /^<[^/!]/;
|
|
122
|
-
const RE_END_TAG = /^<\//;
|
|
123
|
-
|
|
124
|
-
// Pre-compiled patterns for `htmlmin:ignore` block content analysis
|
|
125
|
-
const RE_HTML_COMMENT_START = /^\s*<!--/;
|
|
126
|
-
const RE_CLOSING_TAG_START = /^\s*<\/([a-zA-Z][\w:-]*)/;
|
|
127
|
-
const RE_LAST_HTML_TAG = /[\s\S]*<(\/?[a-zA-Z][\w:-]*)/;
|
|
128
|
-
|
|
129
|
-
// HTML encoding types for annotation-xml (MathML)
|
|
130
|
-
const RE_HTML_ENCODING = /^(text\/html|application\/xhtml\+xml)$/i;
|
|
131
|
-
|
|
132
|
-
// Script merging
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Find the index of the `>` that closes an opening tag, correctly skipping
|
|
136
|
-
* over quoted attribute values (which may contain `>`).
|
|
137
|
-
* @param {string} html
|
|
138
|
-
* @param {number} pos - Start position (just after the tag name)
|
|
139
|
-
* @returns {number} Index of the closing `>`, or -1 if not found
|
|
140
|
-
*/
|
|
141
|
-
function findTagEnd(html, pos) {
|
|
142
|
-
let i = pos;
|
|
143
|
-
while (i < html.length) {
|
|
144
|
-
const ch = html[i];
|
|
145
|
-
if (ch === '>') return i;
|
|
146
|
-
if (ch === '"' || ch === "'") {
|
|
147
|
-
const q = ch;
|
|
148
|
-
i++;
|
|
149
|
-
while (i < html.length && html[i] !== q) i++;
|
|
150
|
-
}
|
|
151
|
-
i++;
|
|
152
|
-
}
|
|
153
|
-
return -1;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Merge consecutive inline script tags into one (`mergeConsecutiveScripts`).
|
|
158
|
-
* Only merges scripts that are compatible:
|
|
159
|
-
* - Both inline (no `src` attribute)
|
|
160
|
-
* - Same `type` (or both default JavaScript)
|
|
161
|
-
* - No conflicting attributes (`async`, `defer`, `nomodule`, different `nonce`)
|
|
162
|
-
*
|
|
163
|
-
* Uses a scanner rather than a regex to locate script boundaries, so literal
|
|
164
|
-
* `</script>` strings inside script content are handled correctly per the HTML
|
|
165
|
-
* spec (raw text ends at the first `</script>`).
|
|
166
|
-
*
|
|
167
|
-
* @param {string} html - The HTML string to process
|
|
168
|
-
* @returns {string} HTML with consecutive scripts merged
|
|
169
|
-
*/
|
|
170
|
-
function mergeConsecutiveScripts(html) {
|
|
171
|
-
// Parse an attribute string into a name→value map
|
|
172
|
-
const parseAttrs = (attrStr) => {
|
|
173
|
-
const attrs = {};
|
|
174
|
-
RE_SCRIPT_ATTRS.lastIndex = 0;
|
|
175
|
-
let m;
|
|
176
|
-
while ((m = RE_SCRIPT_ATTRS.exec(attrStr)) !== null) {
|
|
177
|
-
const name = m[1].toLowerCase();
|
|
178
|
-
const value = m[2] ?? m[3] ?? m[4] ?? '';
|
|
179
|
-
attrs[name] = value;
|
|
180
|
-
}
|
|
181
|
-
return attrs;
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
let changed = true;
|
|
185
|
-
|
|
186
|
-
// Keep merging until no more changes (handles chains of 3+ scripts)
|
|
187
|
-
while (changed) {
|
|
188
|
-
changed = false;
|
|
189
|
-
RE_SCRIPT_OPEN.lastIndex = 0;
|
|
190
|
-
let m1;
|
|
191
|
-
|
|
192
|
-
while ((m1 = RE_SCRIPT_OPEN.exec(html)) !== null) {
|
|
193
|
-
// Use findTagEnd() to get the real closing '>', skipping quoted attribute values
|
|
194
|
-
const tagEnd1 = findTagEnd(html, m1.index + 7);
|
|
195
|
-
if (tagEnd1 === -1) break;
|
|
196
|
-
|
|
197
|
-
const attrs1Str = html.slice(m1.index + 7, tagEnd1);
|
|
198
|
-
const contentStart1 = tagEnd1 + 1;
|
|
199
|
-
|
|
200
|
-
// Find end of this script’s content (first `</script>`—per HTML spec, raw text ends here)
|
|
201
|
-
RE_SCRIPT_CLOSE.lastIndex = contentStart1;
|
|
202
|
-
const close1 = RE_SCRIPT_CLOSE.exec(html);
|
|
203
|
-
if (!close1) break;
|
|
204
|
-
|
|
205
|
-
const content1 = html.slice(contentStart1, close1.index);
|
|
206
|
-
const afterClose1 = close1.index + close1[0].length;
|
|
207
|
-
|
|
208
|
-
// Skip optional whitespace and check for a consecutive <script> tag
|
|
209
|
-
let i = afterClose1;
|
|
210
|
-
while (i < html.length && (html[i] === ' ' || html[i] === '\t' || html[i] === '\n' || html[i] === '\r' || html[i] === '\f')) i++;
|
|
211
|
-
if (html.slice(i, i + 7).toLowerCase() !== '<script' || (html[i + 7] !== '>' && !/\s/.test(html[i + 7]))) {
|
|
212
|
-
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const tagStart2 = i;
|
|
217
|
-
const tagEnd2 = findTagEnd(html, tagStart2 + 7);
|
|
218
|
-
if (tagEnd2 === -1) break;
|
|
219
|
-
|
|
220
|
-
const attrs2Str = html.slice(tagStart2 + 7, tagEnd2);
|
|
221
|
-
const contentStart2 = tagEnd2 + 1;
|
|
222
|
-
|
|
223
|
-
// Find end of second script’s content
|
|
224
|
-
RE_SCRIPT_CLOSE.lastIndex = contentStart2;
|
|
225
|
-
const close2 = RE_SCRIPT_CLOSE.exec(html);
|
|
226
|
-
if (!close2) break;
|
|
227
|
-
|
|
228
|
-
const content2 = html.slice(contentStart2, close2.index);
|
|
229
|
-
const afterClose2 = close2.index + close2[0].length;
|
|
230
|
-
|
|
231
|
-
const a1 = parseAttrs(attrs1Str);
|
|
232
|
-
const a2 = parseAttrs(attrs2Str);
|
|
233
|
-
|
|
234
|
-
// Check for `src`—cannot merge external scripts
|
|
235
|
-
if ('src' in a1 || 'src' in a2) {
|
|
236
|
-
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Check `type` compatibility (both must be default JS)
|
|
241
|
-
// Non-JS types (modules, JSON, etc.) must not be merged:
|
|
242
|
-
// Module scripts have per-script lexical scope, and non-JS content (e.g., JSON)
|
|
243
|
-
// is not concatenable; even identical non-JS types are incompatible
|
|
244
|
-
const type1 = (a1.type || '').toLowerCase();
|
|
245
|
-
const type2 = (a2.type || '').toLowerCase();
|
|
246
|
-
if (!DEFAULT_JS_TYPES.has(type1) || !DEFAULT_JS_TYPES.has(type2)) {
|
|
247
|
-
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Check for conflicting boolean attributes
|
|
252
|
-
let boolConflict = false;
|
|
253
|
-
for (const attr of SCRIPT_BOOL_ATTRS) {
|
|
254
|
-
if ((attr in a1) !== (attr in a2)) { boolConflict = true; break; }
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Check `nonce`—must be same or both absent
|
|
258
|
-
if (boolConflict || a1.nonce !== a2.nonce) {
|
|
259
|
-
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Scripts are compatible—merge them
|
|
264
|
-
changed = true;
|
|
265
|
-
|
|
266
|
-
// Combine content—use semicolon normally, newline only for trailing `//` comments
|
|
267
|
-
const c1 = content1.trim();
|
|
268
|
-
const c2 = content2.trim();
|
|
269
|
-
let mergedContent;
|
|
270
|
-
if (c1 && c2) {
|
|
271
|
-
// Check if last line of c1 contains `//` (single-line comment)
|
|
272
|
-
// If so, use newline to terminate it; otherwise use semicolon (if not already present)
|
|
273
|
-
const lastLine = c1.slice(c1.lastIndexOf('\n') + 1);
|
|
274
|
-
const separator = lastLine.includes('//') ? '\n' : (c1.endsWith(';') ? '' : ';');
|
|
275
|
-
mergedContent = c1 + separator + c2;
|
|
276
|
-
} else {
|
|
277
|
-
mergedContent = c1 || c2;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Use first script’s attributes (they should be compatible)
|
|
281
|
-
html = html.slice(0, m1.index) + `<script${attrs1Str}>${mergedContent}</script>` + html.slice(afterClose2);
|
|
282
|
-
break; // Restart scanning (outer while loop)
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return html;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
57
|
// Type definitions
|
|
290
58
|
|
|
291
59
|
/**
|
|
292
60
|
* @typedef {Object} HTMLAttribute
|
|
293
61
|
* Representation of an attribute from the HTML parser.
|
|
62
|
+
* Internal counterpart: `HTMLAttribute` in `lib/attributes.js`—keep in sync.
|
|
294
63
|
*
|
|
295
64
|
* @prop {string} name
|
|
296
65
|
* @prop {string} [value]
|
|
@@ -700,23 +469,275 @@ function mergeConsecutiveScripts(html) {
|
|
|
700
469
|
* See also: https://perfectionkills.com/experimenting-with-html-minifier/#use_short_doctype
|
|
701
470
|
*
|
|
702
471
|
* Default: `false`
|
|
472
|
+
*
|
|
473
|
+
* @prop {boolean} [insideSVG] - Internal: Set when inside SVG/MathML context
|
|
474
|
+
* @prop {boolean} [insideForeignContent] - Internal: Set when inside SVG `foreignObject`
|
|
475
|
+
* @prop {Function} [parentName] - Internal: Preserved name function during namespace transitions
|
|
476
|
+
* @prop {Function} [htmlName] - Internal: HTML name function preserved from outer context
|
|
703
477
|
*/
|
|
704
478
|
|
|
479
|
+
// Lazy-load heavy dependencies only when needed
|
|
480
|
+
|
|
481
|
+
/** @type {Promise<Function> | undefined} */
|
|
482
|
+
let lightningCSSPromise;
|
|
483
|
+
async function getLightningCSS() {
|
|
484
|
+
if (!lightningCSSPromise) {
|
|
485
|
+
lightningCSSPromise = import('lightningcss').then(m => m.transform);
|
|
486
|
+
}
|
|
487
|
+
return lightningCSSPromise;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** @type {Promise<Function> | undefined} */
|
|
491
|
+
let terserPromise;
|
|
492
|
+
async function getTerser() {
|
|
493
|
+
if (!terserPromise) {
|
|
494
|
+
terserPromise = import('terser').then(m => m.minify);
|
|
495
|
+
}
|
|
496
|
+
return terserPromise;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** @type {Promise<any> | undefined} */
|
|
500
|
+
let swcPromise;
|
|
501
|
+
async function getSwc() {
|
|
502
|
+
if (!swcPromise) {
|
|
503
|
+
swcPromise = import('@swc/core')
|
|
504
|
+
.then(m => m.default || m)
|
|
505
|
+
.catch(() => {
|
|
506
|
+
throw new Error(
|
|
507
|
+
'The swc minifier requires @swc/core to be installed.\n' +
|
|
508
|
+
'Install it with: npm install @swc/core'
|
|
509
|
+
);
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
return swcPromise;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** @type {Promise<Function> | undefined} */
|
|
516
|
+
let svgoPromise;
|
|
517
|
+
async function getSvgo() {
|
|
518
|
+
if (!svgoPromise) {
|
|
519
|
+
svgoPromise = import('svgo').then(m => m.optimize);
|
|
520
|
+
}
|
|
521
|
+
return svgoPromise;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** @type {Promise<Function> | undefined} */
|
|
525
|
+
let decodeHTMLPromise;
|
|
526
|
+
async function getDecodeHTML() {
|
|
527
|
+
if (!decodeHTMLPromise) {
|
|
528
|
+
decodeHTMLPromise = import('entities').then(m => m.decodeHTML);
|
|
529
|
+
}
|
|
530
|
+
return decodeHTMLPromise;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Minification caches (initialized on first use with configurable sizes)
|
|
534
|
+
/** @type {LRU | null} */
|
|
535
|
+
let cssMinifyCache = null;
|
|
536
|
+
/** @type {LRU | null} */
|
|
537
|
+
let jsMinifyCache = null;
|
|
538
|
+
/** @type {LRU | null} */
|
|
539
|
+
let svgMinifyCache = null;
|
|
540
|
+
|
|
541
|
+
// Pre-compiled patterns for script merging (avoid repeated allocation in hot path)
|
|
542
|
+
const RE_SCRIPT_ATTRS = /([^\s=]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
543
|
+
const RE_SCRIPT_OPEN = /<script(?=[\s>])/gi; // Finds tag start; use `findTagEnd()` for the actual closing `>`
|
|
544
|
+
const RE_SCRIPT_CLOSE = /<\/script\s*>/gi;
|
|
545
|
+
const SCRIPT_BOOL_ATTRS = new Set(['async', 'defer', 'nomodule']);
|
|
546
|
+
const DEFAULT_JS_TYPES = new Set(['', 'text/javascript', 'application/javascript']);
|
|
547
|
+
|
|
548
|
+
// Pre-compiled patterns for buffer scanning
|
|
549
|
+
const RE_START_TAG = /^<[^/!]/;
|
|
550
|
+
const RE_END_TAG = /^<\//;
|
|
551
|
+
|
|
552
|
+
// Pre-compiled patterns for `htmlmin:ignore` block content analysis
|
|
553
|
+
const RE_HTML_COMMENT_START = /^\s*<!--/;
|
|
554
|
+
const RE_CLOSING_TAG_START = /^\s*<\/([a-zA-Z][\w:-]*)/;
|
|
555
|
+
const RE_LAST_HTML_TAG = /[\s\S]*<(\/?[a-zA-Z][\w:-]*)/;
|
|
556
|
+
|
|
557
|
+
// HTML encoding types for annotation-xml (MathML)
|
|
558
|
+
const RE_HTML_ENCODING = /^(text\/html|application\/xhtml\+xml)$/i;
|
|
559
|
+
|
|
560
|
+
// Script merging
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Find the index of the `>` that closes an opening tag, correctly skipping
|
|
564
|
+
* over quoted attribute values (which may contain `>`).
|
|
565
|
+
* @param {string} html
|
|
566
|
+
* @param {number} pos - Start position (just after the tag name)
|
|
567
|
+
* @returns {number} Index of the closing `>`, or -1 if not found
|
|
568
|
+
*/
|
|
569
|
+
function findTagEnd(html, pos) {
|
|
570
|
+
let i = pos;
|
|
571
|
+
while (i < html.length) {
|
|
572
|
+
const ch = html[i];
|
|
573
|
+
if (ch === '>') return i;
|
|
574
|
+
if (ch === '"' || ch === "'") {
|
|
575
|
+
const q = ch;
|
|
576
|
+
i++;
|
|
577
|
+
while (i < html.length && html[i] !== q) i++;
|
|
578
|
+
}
|
|
579
|
+
i++;
|
|
580
|
+
}
|
|
581
|
+
return -1;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Merge consecutive inline script tags into one (`mergeConsecutiveScripts`).
|
|
586
|
+
* Only merges scripts that are compatible:
|
|
587
|
+
* - Both inline (no `src` attribute)
|
|
588
|
+
* - Same `type` (or both default JavaScript)
|
|
589
|
+
* - No conflicting attributes (`async`, `defer`, `nomodule`, different `nonce`)
|
|
590
|
+
*
|
|
591
|
+
* Uses a scanner rather than a regex to locate script boundaries, so literal
|
|
592
|
+
* `</script>` strings inside script content are handled correctly per the HTML
|
|
593
|
+
* spec (raw text ends at the first `</script>`).
|
|
594
|
+
*
|
|
595
|
+
* @param {string} html - The HTML string to process
|
|
596
|
+
* @returns {string} HTML with consecutive scripts merged
|
|
597
|
+
*/
|
|
598
|
+
function mergeConsecutiveScripts(html) {
|
|
599
|
+
// Parse an attribute string into a name→value map
|
|
600
|
+
const parseAttrs = (/** @type {string} */ attrStr) => {
|
|
601
|
+
/** @type {Record<string, string>} */
|
|
602
|
+
const attrs = {};
|
|
603
|
+
RE_SCRIPT_ATTRS.lastIndex = 0;
|
|
604
|
+
let m;
|
|
605
|
+
while ((m = RE_SCRIPT_ATTRS.exec(attrStr)) !== null) {
|
|
606
|
+
const name = (m[1] ?? '').toLowerCase();
|
|
607
|
+
const value = m[2] ?? m[3] ?? m[4] ?? '';
|
|
608
|
+
attrs[name] = value;
|
|
609
|
+
}
|
|
610
|
+
return attrs;
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
let changed = true;
|
|
614
|
+
|
|
615
|
+
// Keep merging until no more changes (handles chains of 3+ scripts)
|
|
616
|
+
while (changed) {
|
|
617
|
+
changed = false;
|
|
618
|
+
RE_SCRIPT_OPEN.lastIndex = 0;
|
|
619
|
+
let m1;
|
|
620
|
+
|
|
621
|
+
while ((m1 = RE_SCRIPT_OPEN.exec(html)) !== null) {
|
|
622
|
+
// Use findTagEnd() to get the real closing '>', skipping quoted attribute values
|
|
623
|
+
const tagEnd1 = findTagEnd(html, m1.index + 7);
|
|
624
|
+
if (tagEnd1 === -1) break;
|
|
625
|
+
|
|
626
|
+
const attrs1Str = html.slice(m1.index + 7, tagEnd1);
|
|
627
|
+
const contentStart1 = tagEnd1 + 1;
|
|
628
|
+
|
|
629
|
+
// Find end of this script’s content (first `</script>`—per HTML spec, raw text ends here)
|
|
630
|
+
RE_SCRIPT_CLOSE.lastIndex = contentStart1;
|
|
631
|
+
const close1 = RE_SCRIPT_CLOSE.exec(html);
|
|
632
|
+
if (!close1) break;
|
|
633
|
+
|
|
634
|
+
const content1 = html.slice(contentStart1, close1.index);
|
|
635
|
+
const afterClose1 = close1.index + close1[0].length;
|
|
636
|
+
|
|
637
|
+
// Skip optional whitespace and check for a consecutive <script> tag
|
|
638
|
+
let i = afterClose1;
|
|
639
|
+
while (i < html.length && (html[i] === ' ' || html[i] === '\t' || html[i] === '\n' || html[i] === '\r' || html[i] === '\f')) i++;
|
|
640
|
+
if (html.slice(i, i + 7).toLowerCase() !== '<script' || (html.charAt(i + 7) !== '>' && !/\s/.test(html.charAt(i + 7)))) {
|
|
641
|
+
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const tagStart2 = i;
|
|
646
|
+
const tagEnd2 = findTagEnd(html, tagStart2 + 7);
|
|
647
|
+
if (tagEnd2 === -1) break;
|
|
648
|
+
|
|
649
|
+
const attrs2Str = html.slice(tagStart2 + 7, tagEnd2);
|
|
650
|
+
const contentStart2 = tagEnd2 + 1;
|
|
651
|
+
|
|
652
|
+
// Find end of second script’s content
|
|
653
|
+
RE_SCRIPT_CLOSE.lastIndex = contentStart2;
|
|
654
|
+
const close2 = RE_SCRIPT_CLOSE.exec(html);
|
|
655
|
+
if (!close2) break;
|
|
656
|
+
|
|
657
|
+
const content2 = html.slice(contentStart2, close2.index);
|
|
658
|
+
const afterClose2 = close2.index + close2[0].length;
|
|
659
|
+
|
|
660
|
+
const a1 = parseAttrs(attrs1Str);
|
|
661
|
+
const a2 = parseAttrs(attrs2Str);
|
|
662
|
+
|
|
663
|
+
// Check for `src`—cannot merge external scripts
|
|
664
|
+
if ('src' in a1 || 'src' in a2) {
|
|
665
|
+
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Check `type` compatibility (both must be default JS)
|
|
670
|
+
// Non-JS types (modules, JSON, etc.) must not be merged:
|
|
671
|
+
// Module scripts have per-script lexical scope, and non-JS content (e.g., JSON)
|
|
672
|
+
// is not concatenable; even identical non-JS types are incompatible
|
|
673
|
+
const type1 = (a1.type || '').toLowerCase();
|
|
674
|
+
const type2 = (a2.type || '').toLowerCase();
|
|
675
|
+
if (!DEFAULT_JS_TYPES.has(type1) || !DEFAULT_JS_TYPES.has(type2)) {
|
|
676
|
+
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Check for conflicting boolean attributes
|
|
681
|
+
let boolConflict = false;
|
|
682
|
+
for (const attr of SCRIPT_BOOL_ATTRS) {
|
|
683
|
+
if ((attr in a1) !== (attr in a2)) { boolConflict = true; break; }
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Check `nonce`—must be same or both absent
|
|
687
|
+
if (boolConflict || a1.nonce !== a2.nonce) {
|
|
688
|
+
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Scripts are compatible—merge them
|
|
693
|
+
changed = true;
|
|
694
|
+
|
|
695
|
+
// Combine content—use semicolon normally, newline only for trailing `//` comments
|
|
696
|
+
const c1 = content1.trim();
|
|
697
|
+
const c2 = content2.trim();
|
|
698
|
+
let mergedContent;
|
|
699
|
+
if (c1 && c2) {
|
|
700
|
+
// Check if last line of c1 contains `//` (single-line comment)
|
|
701
|
+
// If so, use newline to terminate it; otherwise use semicolon (if not already present)
|
|
702
|
+
const lastLine = c1.slice(c1.lastIndexOf('\n') + 1);
|
|
703
|
+
const separator = lastLine.includes('//') ? '\n' : (c1.endsWith(';') ? '' : ';');
|
|
704
|
+
mergedContent = c1 + separator + c2;
|
|
705
|
+
} else {
|
|
706
|
+
mergedContent = c1 || c2;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Use first script’s attributes (they should be compatible)
|
|
710
|
+
html = html.slice(0, m1.index) + `<script${attrs1Str}>${mergedContent}</script>` + html.slice(afterClose2);
|
|
711
|
+
break; // Restart scanning (outer while loop)
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
return html;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* @param {string} value
|
|
720
|
+
* @param {MinifierOptions} options
|
|
721
|
+
* @param {string} uidIgnore
|
|
722
|
+
* @param {string} uidAttr
|
|
723
|
+
* @param {string[]} ignoredMarkupChunks
|
|
724
|
+
*/
|
|
705
725
|
async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupChunks) {
|
|
706
726
|
const attrChains = options.sortAttributes && typeof options.sortAttributes !== 'function' && Object.create(null);
|
|
707
727
|
const classChain = options.sortClassNames && typeof options.sortClassNames !== 'function' && new TokenChain();
|
|
728
|
+
const resolveName = /** @type {(name: string) => string} */ (options.name);
|
|
708
729
|
|
|
709
|
-
function attrNames(attrs) {
|
|
730
|
+
function attrNames(/** @type {HTMLAttribute[]} */ attrs) {
|
|
710
731
|
return attrs.map(function (attr) {
|
|
711
|
-
return
|
|
732
|
+
return resolveName(attr.name);
|
|
712
733
|
});
|
|
713
734
|
}
|
|
714
735
|
|
|
715
|
-
function shouldSkipUID(token, uid) {
|
|
736
|
+
function shouldSkipUID(/** @type {string} */ token, /** @type {string} */ uid) {
|
|
716
737
|
return !uid || token.indexOf(uid) === -1;
|
|
717
738
|
}
|
|
718
739
|
|
|
719
|
-
function shouldKeepToken(token) {
|
|
740
|
+
function shouldKeepToken(/** @type {string} */ token) {
|
|
720
741
|
// Filter out any HTML comment tokens (UID placeholders)
|
|
721
742
|
// These are temporary markers created by `htmlmin:ignore` and `ignoreCustomFragments`
|
|
722
743
|
if (token.startsWith('<!--') && token.endsWith('-->')) {
|
|
@@ -730,10 +751,13 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
730
751
|
const whitespaceSplitPatternScan = /[ \t\n\f\r]+/;
|
|
731
752
|
const whitespaceSplitPatternSort = /[ \n\f\r]+/;
|
|
732
753
|
|
|
733
|
-
async function scan(input) {
|
|
734
|
-
|
|
754
|
+
async function scan(/** @type {string} */ input) {
|
|
755
|
+
/** @type {string} */
|
|
756
|
+
let currentTag;
|
|
757
|
+
/** @type {string | undefined} */
|
|
758
|
+
let currentType;
|
|
735
759
|
const parser = new HTMLParser(input, {
|
|
736
|
-
start: function (tag, attrs) {
|
|
760
|
+
start: function (/** @type {string} */ tag, /** @type {HTMLAttribute[]} */ attrs) {
|
|
737
761
|
if (attrChains) {
|
|
738
762
|
if (!attrChains[tag]) {
|
|
739
763
|
attrChains[tag] = new TokenChain();
|
|
@@ -741,9 +765,8 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
741
765
|
const attrNamesList = attrNames(attrs).filter(shouldKeepToken);
|
|
742
766
|
attrChains[tag].add(attrNamesList);
|
|
743
767
|
}
|
|
744
|
-
for (
|
|
745
|
-
|
|
746
|
-
if (classChain && attr.value && options.name(attr.name) === 'class') {
|
|
768
|
+
for (const attr of attrs) {
|
|
769
|
+
if (classChain && attr.value && resolveName(attr.name) === 'class') {
|
|
747
770
|
const classes = trimWhitespace(attr.value).split(whitespaceSplitPatternScan).filter(shouldKeepToken);
|
|
748
771
|
classChain.add(classes);
|
|
749
772
|
} else if (options.processScripts && attr.name.toLowerCase() === 'type') {
|
|
@@ -755,11 +778,11 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
755
778
|
end: function () {
|
|
756
779
|
currentTag = '';
|
|
757
780
|
},
|
|
758
|
-
chars: async function (text) {
|
|
781
|
+
chars: async function (/** @type {string} */ text) {
|
|
759
782
|
// Only recursively scan HTML content, not JSON-LD or other non-HTML script types
|
|
760
783
|
// `scan()` is for analyzing HTML attribute order, not for parsing JSON
|
|
761
784
|
if (options.processScripts && specialContentElements.has(currentTag) &&
|
|
762
|
-
options.processScripts.indexOf(currentType) > -1 &&
|
|
785
|
+
options.processScripts.indexOf(currentType ?? '') > -1 &&
|
|
763
786
|
currentType === 'text/html') {
|
|
764
787
|
await scan(text);
|
|
765
788
|
}
|
|
@@ -818,7 +841,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
818
841
|
// Expand UID tokens back to the original content for frequency analysis
|
|
819
842
|
let expandedValue = value;
|
|
820
843
|
if (uidReplacePattern) {
|
|
821
|
-
expandedValue = value.replace(uidReplacePattern, function (
|
|
844
|
+
expandedValue = value.replace(uidReplacePattern, function (/** @type {string} */ _match, /** @type {string} */ index) {
|
|
822
845
|
return ignoredMarkupChunks[+index] || '';
|
|
823
846
|
});
|
|
824
847
|
// Reset `lastIndex` for pattern reuse
|
|
@@ -839,7 +862,11 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
839
862
|
await scan(scanValue);
|
|
840
863
|
} finally {
|
|
841
864
|
// Restore original option
|
|
842
|
-
|
|
865
|
+
if (originalContinueOnParseError !== undefined) {
|
|
866
|
+
options.continueOnParseError = originalContinueOnParseError;
|
|
867
|
+
} else {
|
|
868
|
+
delete options.continueOnParseError;
|
|
869
|
+
}
|
|
843
870
|
}
|
|
844
871
|
if (attrChains) {
|
|
845
872
|
const attrSorters = Object.create(null);
|
|
@@ -849,14 +876,14 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
849
876
|
// Memoize sorted attribute orders—attribute sets often repeat in templates
|
|
850
877
|
const attrOrderCache = new LRU(500);
|
|
851
878
|
|
|
852
|
-
options.sortAttributes = function (tag, attrs) {
|
|
879
|
+
options.sortAttributes = function (/** @type {string} */ tag, /** @type {HTMLAttribute[]} */ attrs) {
|
|
853
880
|
const sorter = attrSorters[tag];
|
|
854
881
|
if (sorter) {
|
|
855
882
|
const names = attrNames(attrs);
|
|
856
883
|
|
|
857
884
|
// Create order-independent cache key from tag and sorted attribute names
|
|
858
885
|
const cacheKey = tag + ':' + names.slice().sort().join(',');
|
|
859
|
-
let sortedNames = attrOrderCache.get(cacheKey);
|
|
886
|
+
let sortedNames = /** @type {string[] | undefined} */ (attrOrderCache.get(cacheKey));
|
|
860
887
|
|
|
861
888
|
if (sortedNames === undefined) {
|
|
862
889
|
// Only sort if not in cache—need to clone names since sort mutates in place
|
|
@@ -866,10 +893,10 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
866
893
|
|
|
867
894
|
// Apply the sorted order to `attrs`
|
|
868
895
|
const attrMap = Object.create(null);
|
|
869
|
-
names.forEach(function (name, index) {
|
|
896
|
+
names.forEach(function (/** @type {string} */ name, /** @type {number} */ index) {
|
|
870
897
|
(attrMap[name] || (attrMap[name] = [])).push(attrs[index]);
|
|
871
898
|
});
|
|
872
|
-
sortedNames.forEach(function (name, index) {
|
|
899
|
+
/** @type {string[]} */ (sortedNames).forEach(function (/** @type {string} */ name, /** @type {number} */ index) {
|
|
873
900
|
attrs[index] = attrMap[name].shift();
|
|
874
901
|
});
|
|
875
902
|
}
|
|
@@ -880,14 +907,14 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
880
907
|
// Memoize `sortClassNames` results—class lists often repeat in templates
|
|
881
908
|
const classNameCache = new LRU(500);
|
|
882
909
|
|
|
883
|
-
options.sortClassNames = function (value) {
|
|
910
|
+
options.sortClassNames = function (/** @type {string} */ value) {
|
|
884
911
|
// Fast path: Single class (no spaces) needs no sorting
|
|
885
912
|
if (value.indexOf(' ') === -1) {
|
|
886
913
|
return value;
|
|
887
914
|
}
|
|
888
915
|
|
|
889
916
|
// Check cache first
|
|
890
|
-
const cached = classNameCache.get(value);
|
|
917
|
+
const cached = /** @type {string | undefined} */ (classNameCache.get(value));
|
|
891
918
|
if (cached !== undefined) {
|
|
892
919
|
return cached;
|
|
893
920
|
}
|
|
@@ -896,13 +923,13 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
896
923
|
// Fast path: Skip if no HTML comments (UID markers) present
|
|
897
924
|
let expandedValue = value;
|
|
898
925
|
if (uidReplacePattern && value.indexOf('<!--') !== -1) {
|
|
899
|
-
expandedValue = value.replace(uidReplacePattern, function (
|
|
926
|
+
expandedValue = value.replace(uidReplacePattern, function (/** @type {string} */ _match, /** @type {string} */ index) {
|
|
900
927
|
return ignoredMarkupChunks[+index] || '';
|
|
901
928
|
});
|
|
902
929
|
// Reset `lastIndex` for pattern reuse
|
|
903
930
|
uidReplacePattern.lastIndex = 0;
|
|
904
931
|
}
|
|
905
|
-
const classes = expandedValue.split(whitespaceSplitPatternSort).filter(function(cls) {
|
|
932
|
+
const classes = expandedValue.split(whitespaceSplitPatternSort).filter(function(/** @type {string} */ cls) {
|
|
906
933
|
return cls !== '';
|
|
907
934
|
});
|
|
908
935
|
const sorted = sorter.sort(classes);
|
|
@@ -931,29 +958,46 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
931
958
|
value = collapseWhitespace(value, options, true, true);
|
|
932
959
|
}
|
|
933
960
|
|
|
961
|
+
const resolveName = /** @type {(name: string) => string} */ (options.name);
|
|
962
|
+
/** @type {HTMLAttribute[]} */
|
|
963
|
+
const emptyAttrs = [];
|
|
964
|
+
|
|
965
|
+
/** @type {string[]} */
|
|
934
966
|
const buffer = [];
|
|
935
|
-
|
|
967
|
+
/** @type {string} */
|
|
968
|
+
let charsPrevTag = '';
|
|
936
969
|
let currentChars = '';
|
|
937
|
-
|
|
970
|
+
/** @type {boolean} */
|
|
971
|
+
let hasChars = false;
|
|
938
972
|
let currentTag = '';
|
|
973
|
+
/** @type {Array<{name: string, value?: string, quote?: string, customAssign?: string, customOpen?: string, customClose?: string}>} */
|
|
939
974
|
let currentAttrs = [];
|
|
975
|
+
/** @type {string[]} */
|
|
940
976
|
const stackNoTrimWhitespace = [];
|
|
977
|
+
/** @type {string[]} */
|
|
941
978
|
const stackNoCollapseWhitespace = [];
|
|
942
979
|
let preTextareaDepth = 0; // Count of `pre`/`textarea` entries in `stackNoTrimWhitespace`
|
|
943
980
|
let optionalStartTag = '';
|
|
944
981
|
let optionalEndTag = '';
|
|
945
982
|
let optionalEndTagEmitted = false;
|
|
983
|
+
/** @type {string[]} */
|
|
946
984
|
const ignoredMarkupChunks = [];
|
|
985
|
+
/** @type {Array<RegExpExecArray | null>} */
|
|
947
986
|
const ignoredCustomMarkupChunks = [];
|
|
987
|
+
/** @type {string | undefined} */
|
|
948
988
|
let uidIgnore;
|
|
989
|
+
/** @type {RegExp | undefined} */
|
|
949
990
|
let uidIgnorePlaceholderPattern;
|
|
991
|
+
/** @type {string | undefined} */
|
|
950
992
|
let uidAttr;
|
|
993
|
+
/** @type {RegExp | undefined} */
|
|
951
994
|
let uidPattern;
|
|
995
|
+
/** @type {RegExp | undefined} */
|
|
952
996
|
let uidAttrLeadingPattern;
|
|
953
997
|
// Create inline tags/text sets with custom elements
|
|
954
998
|
const customElementsInput = options.inlineCustomElements ?? [];
|
|
955
999
|
const customElementsArr = Array.isArray(customElementsInput) ? customElementsInput : Array.from(customElementsInput);
|
|
956
|
-
const normalizedCustomElements = customElementsArr.map(name =>
|
|
1000
|
+
const normalizedCustomElements = customElementsArr.map(name => resolveName(name));
|
|
957
1001
|
// Fast path: Reuse base sets if no custom elements
|
|
958
1002
|
const inlineTextSet = normalizedCustomElements.length
|
|
959
1003
|
? new Set([...inlineElementsToKeepWhitespaceWithin, ...normalizedCustomElements])
|
|
@@ -963,6 +1007,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
963
1007
|
: inlineElementsToKeepWhitespaceAround;
|
|
964
1008
|
|
|
965
1009
|
// Parse `removeEmptyElementsExcept` option
|
|
1010
|
+
/** @type {Array<{tag: string, attrs: {[x: string]: string | undefined} | null}>} */
|
|
966
1011
|
let removeEmptyElementsExcept;
|
|
967
1012
|
if (options.removeEmptyElementsExcept && !Array.isArray(options.removeEmptyElementsExcept)) {
|
|
968
1013
|
if (options.log) {
|
|
@@ -970,7 +1015,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
970
1015
|
}
|
|
971
1016
|
removeEmptyElementsExcept = [];
|
|
972
1017
|
} else {
|
|
973
|
-
removeEmptyElementsExcept = parseRemoveEmptyElementsExcept(options.removeEmptyElementsExcept, options) || [];
|
|
1018
|
+
removeEmptyElementsExcept = parseRemoveEmptyElementsExcept(options.removeEmptyElementsExcept || [], /** @type {any} */ (options)) || [];
|
|
974
1019
|
}
|
|
975
1020
|
|
|
976
1021
|
// Temporarily replace ignored chunks with comments, so that there’s no need to worry what’s there;
|
|
@@ -1012,17 +1057,17 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1012
1057
|
// This allows proper frequency analysis with access to ignored content via UID tokens
|
|
1013
1058
|
if ((options.sortAttributes && typeof options.sortAttributes !== 'function') ||
|
|
1014
1059
|
(options.sortClassNames && typeof options.sortClassNames !== 'function')) {
|
|
1015
|
-
await createSortFns(value, options, uidIgnore,
|
|
1060
|
+
await createSortFns(value, options, uidIgnore ?? '', uidAttr ?? '', ignoredMarkupChunks);
|
|
1016
1061
|
}
|
|
1017
1062
|
|
|
1018
|
-
const customFragments = options.ignoreCustomFragments.map(function (re) {
|
|
1063
|
+
const customFragments = (options.ignoreCustomFragments || []).map(function (/** @type {RegExp} */ re) {
|
|
1019
1064
|
return re.source;
|
|
1020
1065
|
});
|
|
1021
1066
|
if (customFragments.length) {
|
|
1022
1067
|
// Warn about potential ReDoS if custom fragments use unlimited quantifiers
|
|
1023
|
-
for (
|
|
1024
|
-
if (/[*+]/.test(
|
|
1025
|
-
options.log('Warning: Custom fragment contains unlimited quantifiers (“*” or “+”) which may cause ReDoS vulnerability');
|
|
1068
|
+
for (const fragment of customFragments) {
|
|
1069
|
+
if (/[*+]/.test(fragment)) {
|
|
1070
|
+
options.log?.('Warning: Custom fragment contains unlimited quantifiers (“*” or “+”) which may cause ReDoS vulnerability');
|
|
1026
1071
|
break;
|
|
1027
1072
|
}
|
|
1028
1073
|
}
|
|
@@ -1050,28 +1095,28 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1050
1095
|
uidPattern = new RegExp('(\\s*)' + uidAttr + '([0-9]+)' + uidAttr + '(\\s*)', 'g');
|
|
1051
1096
|
uidAttrLeadingPattern = new RegExp('^\\s*' + uidAttr + '(\\d+)' + uidAttr);
|
|
1052
1097
|
|
|
1053
|
-
if (options.minifyCSS) {
|
|
1054
|
-
options.minifyCSS = (function (fn) {
|
|
1055
|
-
return function (text, type) {
|
|
1056
|
-
text = text.replace(uidPattern, function (
|
|
1098
|
+
if (options.minifyCSS !== identity) {
|
|
1099
|
+
/** @type {any} */ (options).minifyCSS = (function (/** @type {Function} */ fn) {
|
|
1100
|
+
return function (/** @type {string} */ text, /** @type {string} */ type) {
|
|
1101
|
+
text = text.replace(/** @type {RegExp} */ (uidPattern), function (/** @type {string} */ _match, /** @type {string} */ _prefix, /** @type {string} */ index) {
|
|
1057
1102
|
const chunks = ignoredCustomMarkupChunks[+index];
|
|
1058
|
-
return chunks[1] + uidAttr + index + uidAttr + chunks[2];
|
|
1103
|
+
return (chunks?.[1] ?? '') + uidAttr + index + uidAttr + (chunks?.[2] ?? '');
|
|
1059
1104
|
});
|
|
1060
1105
|
|
|
1061
1106
|
return fn(text, type);
|
|
1062
1107
|
};
|
|
1063
|
-
})(options.minifyCSS);
|
|
1108
|
+
})(/** @type {Function} */ (options.minifyCSS));
|
|
1064
1109
|
}
|
|
1065
1110
|
|
|
1066
|
-
if (options.minifyJS) {
|
|
1067
|
-
options.minifyJS = (function (fn) {
|
|
1068
|
-
return function (text, inline, isModule) {
|
|
1069
|
-
return fn(text.replace(uidPattern, function (
|
|
1111
|
+
if (options.minifyJS !== identity) {
|
|
1112
|
+
options.minifyJS = (function (/** @type {Function} */ fn) {
|
|
1113
|
+
return function (/** @type {string} */ text, /** @type {boolean} */ inline, /** @type {boolean} */ isModule) {
|
|
1114
|
+
return fn(text.replace(/** @type {RegExp} */ (uidPattern), function (/** @type {string} */ _match, /** @type {string} */ _prefix, /** @type {string} */ index) {
|
|
1070
1115
|
const chunks = ignoredCustomMarkupChunks[+index];
|
|
1071
|
-
return chunks[1] + uidAttr + index + uidAttr + chunks[2];
|
|
1116
|
+
return (chunks?.[1] ?? '') + uidAttr + index + uidAttr + (chunks?.[2] ?? '');
|
|
1072
1117
|
}), inline, isModule);
|
|
1073
1118
|
};
|
|
1074
|
-
})(options.minifyJS);
|
|
1119
|
+
})(/** @type {Function} */ (options.minifyJS));
|
|
1075
1120
|
}
|
|
1076
1121
|
}
|
|
1077
1122
|
|
|
@@ -1082,17 +1127,17 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1082
1127
|
}
|
|
1083
1128
|
}
|
|
1084
1129
|
|
|
1085
|
-
function canCollapseWhitespace(tag, attrs) {
|
|
1086
|
-
return options.canCollapseWhitespace(tag, attrs, defaultCanCollapseWhitespace);
|
|
1130
|
+
function canCollapseWhitespace(/** @type {string} */ tag, /** @type {HTMLAttribute[]} */ attrs) {
|
|
1131
|
+
return /** @type {Function} */ (options.canCollapseWhitespace)(tag, attrs, defaultCanCollapseWhitespace);
|
|
1087
1132
|
}
|
|
1088
1133
|
|
|
1089
|
-
function canTrimWhitespace(tag, attrs) {
|
|
1090
|
-
return options.canTrimWhitespace(tag, attrs, defaultCanTrimWhitespace);
|
|
1134
|
+
function canTrimWhitespace(/** @type {string} */ tag, /** @type {HTMLAttribute[]} */ attrs) {
|
|
1135
|
+
return /** @type {Function} */ (options.canTrimWhitespace)(tag, attrs, defaultCanTrimWhitespace);
|
|
1091
1136
|
}
|
|
1092
1137
|
|
|
1093
1138
|
function removeStartTag() {
|
|
1094
1139
|
let index = buffer.length - 1;
|
|
1095
|
-
while (index > 0 && !RE_START_TAG.test(buffer[index])) {
|
|
1140
|
+
while (index > 0 && !RE_START_TAG.test(buffer[index] ?? '')) {
|
|
1096
1141
|
index--;
|
|
1097
1142
|
}
|
|
1098
1143
|
buffer.length = Math.max(0, index);
|
|
@@ -1100,20 +1145,20 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1100
1145
|
|
|
1101
1146
|
function removeEndTag() {
|
|
1102
1147
|
let index = buffer.length - 1;
|
|
1103
|
-
while (index > 0 && !RE_END_TAG.test(buffer[index])) {
|
|
1148
|
+
while (index > 0 && !RE_END_TAG.test(buffer[index] ?? '')) {
|
|
1104
1149
|
index--;
|
|
1105
1150
|
}
|
|
1106
1151
|
buffer.length = Math.max(0, index);
|
|
1107
1152
|
}
|
|
1108
1153
|
|
|
1109
1154
|
// Look for trailing whitespaces, bypass any inline tags
|
|
1110
|
-
function trimTrailingWhitespace(index, nextTag) {
|
|
1111
|
-
for (let endTag =
|
|
1112
|
-
const str = buffer[index];
|
|
1155
|
+
function trimTrailingWhitespace(/** @type {number} */ index, /** @type {string} */ nextTag) {
|
|
1156
|
+
for (let endTag = ''; index >= 0 && canTrimWhitespace(endTag, emptyAttrs); index--) {
|
|
1157
|
+
const str = buffer[index] ?? '';
|
|
1113
1158
|
const match = str.match(/^<\/([\w:-]+)>$/);
|
|
1114
1159
|
if (match) {
|
|
1115
|
-
endTag = match[1];
|
|
1116
|
-
} else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str,
|
|
1160
|
+
endTag = match[1] ?? '';
|
|
1161
|
+
} else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, '', nextTag, emptyAttrs, emptyAttrs, options, inlineElements, inlineTextSet))) {
|
|
1117
1162
|
break;
|
|
1118
1163
|
}
|
|
1119
1164
|
}
|
|
@@ -1122,10 +1167,10 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1122
1167
|
// Look for trailing whitespaces from previously processed text
|
|
1123
1168
|
// which may not be trimmed due to a following comment or an empty
|
|
1124
1169
|
// element which has now been removed
|
|
1125
|
-
function squashTrailingWhitespace(nextTag) {
|
|
1170
|
+
function squashTrailingWhitespace(/** @type {string} */ nextTag) {
|
|
1126
1171
|
let charsIndex = buffer.length - 1;
|
|
1127
1172
|
if (buffer.length > 1) {
|
|
1128
|
-
const item = buffer[buffer.length - 1];
|
|
1173
|
+
const item = buffer[buffer.length - 1] ?? '';
|
|
1129
1174
|
if (/^(?:<!|$)/.test(item) && (!uidIgnore || item.indexOf(uidIgnore) === -1)) {
|
|
1130
1175
|
charsIndex--;
|
|
1131
1176
|
}
|
|
@@ -1134,6 +1179,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1134
1179
|
}
|
|
1135
1180
|
|
|
1136
1181
|
// SVG subtree capture: When SVGO is active, record buffer positions for post-processing
|
|
1182
|
+
/** @type {Array<{start: number, end: number}>} */
|
|
1137
1183
|
const svgBlocks = []; // Array of { start, end } buffer indices
|
|
1138
1184
|
let svgBufferStartIndex = -1;
|
|
1139
1185
|
let svgDepth = 0;
|
|
@@ -1146,7 +1192,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1146
1192
|
// Compute `nextTag` only when whitespace collapse features require it
|
|
1147
1193
|
wantsNextTag: !!(options.collapseWhitespace || options.collapseInlineTagWhitespace || options.conservativeCollapse),
|
|
1148
1194
|
|
|
1149
|
-
start: async function (tag, attrs, unary, unarySlash, autoGenerated) {
|
|
1195
|
+
start: async function (/** @type {string} */ tag, /** @type {HTMLAttribute[]} */ attrs, /** @type {boolean} */ unary, /** @type {string} */ unarySlash, /** @type {boolean} */ autoGenerated) {
|
|
1150
1196
|
const lowerTag = tag.toLowerCase();
|
|
1151
1197
|
if (lowerTag === 'svg' || lowerTag === 'math') {
|
|
1152
1198
|
options = Object.create(options);
|
|
@@ -1168,21 +1214,21 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1168
1214
|
// Note: The element itself is in SVG/MathML namespace, only its children are HTML
|
|
1169
1215
|
let useParentNameForTag = false;
|
|
1170
1216
|
if (options.insideForeignContent && (lowerTag === 'foreignobject' ||
|
|
1171
|
-
(lowerTag === 'annotation-xml' && attrs.some(a => a.name.toLowerCase() === 'encoding' &&
|
|
1172
|
-
RE_HTML_ENCODING.test(a.value))))) {
|
|
1173
|
-
const parentName = options.name;
|
|
1217
|
+
(lowerTag === 'annotation-xml' && attrs.some((/** @type {HTMLAttribute} */ a) => a.name.toLowerCase() === 'encoding' &&
|
|
1218
|
+
RE_HTML_ENCODING.test(a.value ?? ''))))) {
|
|
1219
|
+
const parentName = /** @type {(name: string) => string} */ (options.name);
|
|
1174
1220
|
options = Object.create(options);
|
|
1175
1221
|
options.caseSensitive = false;
|
|
1176
1222
|
options.keepClosingSlash = false;
|
|
1177
1223
|
options.parentName = parentName; // Preserve for the element tag itself
|
|
1178
|
-
options.name = options.htmlName
|
|
1224
|
+
options.name = /** @type {(name: string) => string} */ (options.htmlName ?? lowercase);
|
|
1179
1225
|
options.insideForeignContent = false;
|
|
1180
1226
|
// Note: `removeAttributeQuotes`, `removeTagWhitespace`, and `decodeEntities`
|
|
1181
1227
|
// stay disabled (inherited from SVG context) because the entire SVG block
|
|
1182
1228
|
// must be valid XML for SVGO processing
|
|
1183
1229
|
useParentNameForTag = true;
|
|
1184
1230
|
}
|
|
1185
|
-
tag = (useParentNameForTag ? options.parentName : options.name)(tag);
|
|
1231
|
+
tag = /** @type {(name: string) => string} */ (useParentNameForTag ? options.parentName : options.name)(tag);
|
|
1186
1232
|
currentTag = tag;
|
|
1187
1233
|
charsPrevTag = tag;
|
|
1188
1234
|
if (!inlineTextSet.has(tag)) {
|
|
@@ -1248,10 +1294,10 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1248
1294
|
// Remove duplicate attributes (per HTML spec, first occurrence wins)
|
|
1249
1295
|
// Duplicate attributes result in invalid HTML
|
|
1250
1296
|
// https://html.spec.whatwg.org/multipage/parsing.html#attribute-name-state
|
|
1251
|
-
deduplicateAttributes(attrs, options.caseSensitive);
|
|
1297
|
+
deduplicateAttributes(attrs, Boolean(options.caseSensitive));
|
|
1252
1298
|
|
|
1253
1299
|
if (options.sortAttributes) {
|
|
1254
|
-
options.sortAttributes(tag, attrs);
|
|
1300
|
+
/** @type {Function} */ (options.sortAttributes)(tag, attrs);
|
|
1255
1301
|
}
|
|
1256
1302
|
|
|
1257
1303
|
const attrResults = attrs.map(attr => normalizeAttr(attr, attrs, tag, options, minifyHTML));
|
|
@@ -1281,7 +1327,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1281
1327
|
currentTag = '';
|
|
1282
1328
|
}
|
|
1283
1329
|
},
|
|
1284
|
-
end: function (tag, attrs, autoGenerated) {
|
|
1330
|
+
end: function (/** @type {string} */ tag, /** @type {HTMLAttribute[]} */ attrs, /** @type {boolean} */ autoGenerated) {
|
|
1285
1331
|
const lowerTag = tag.toLowerCase();
|
|
1286
1332
|
// Restore parent context when exiting SVG/MathML or HTML-in-foreign-content elements
|
|
1287
1333
|
if (lowerTag === 'svg' || lowerTag === 'math') {
|
|
@@ -1290,7 +1336,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1290
1336
|
!options.insideForeignContent && Object.getPrototypeOf(options).insideForeignContent) {
|
|
1291
1337
|
options = Object.getPrototypeOf(options);
|
|
1292
1338
|
}
|
|
1293
|
-
tag = options.name(tag);
|
|
1339
|
+
tag = /** @type {Function} */ (options.name)(tag);
|
|
1294
1340
|
|
|
1295
1341
|
// Check if current tag is in a whitespace stack
|
|
1296
1342
|
if (options.collapseWhitespace) {
|
|
@@ -1336,7 +1382,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1336
1382
|
let preserve = false;
|
|
1337
1383
|
if (removeEmptyElementsExcept.length) {
|
|
1338
1384
|
// Normalize attribute names for comparison with specs
|
|
1339
|
-
const normalizedAttrs = attrs.map(attr => ({ ...attr, name:
|
|
1385
|
+
const normalizedAttrs = attrs.map((/** @type {HTMLAttribute} */ attr) => ({ ...attr, name: resolveName(attr.name) }));
|
|
1340
1386
|
preserve = shouldPreserveEmptyElement(tag, normalizedAttrs, removeEmptyElementsExcept);
|
|
1341
1387
|
}
|
|
1342
1388
|
|
|
@@ -1383,7 +1429,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1383
1429
|
}
|
|
1384
1430
|
}
|
|
1385
1431
|
},
|
|
1386
|
-
chars: function (text, prevTag, nextTag, prevAttrs, nextAttrs) {
|
|
1432
|
+
chars: function (/** @type {string} */ text, /** @type {string} */ prevTag, /** @type {string} */ nextTag, /** @type {HTMLAttribute[]} */ prevAttrs, /** @type {HTMLAttribute[]} */ nextAttrs) {
|
|
1387
1433
|
prevTag = prevTag === '' ? 'comment' : prevTag;
|
|
1388
1434
|
nextTag = nextTag === '' ? 'comment' : nextTag;
|
|
1389
1435
|
prevAttrs = prevAttrs || [];
|
|
@@ -1399,7 +1445,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1399
1445
|
const needsMinifyCSS = options.minifyCSS !== identity && isStyleElement(currentTag, currentAttrs);
|
|
1400
1446
|
|
|
1401
1447
|
// Whitespace collapsing phase (sync); captures `prevTag`/`nextTag`/`prevAttrs`/`nextAttrs` from outer scope
|
|
1402
|
-
function charsCollapse(text) {
|
|
1448
|
+
function charsCollapse(/** @type {string} */ text) {
|
|
1403
1449
|
// Trim outermost newline-based whitespace inside `pre`/`textarea` elements
|
|
1404
1450
|
// This removes trailing newlines often added by template engines before closing tags
|
|
1405
1451
|
// Only trims single trailing newlines (multiple newlines are likely intentional formatting)
|
|
@@ -1421,7 +1467,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1421
1467
|
// to avoid side effects on the `inlineTextSet` branch below
|
|
1422
1468
|
let effectivePrevTag = prevTag;
|
|
1423
1469
|
if (prevTag === 'comment') {
|
|
1424
|
-
const prevComment = buffer[buffer.length - 1];
|
|
1470
|
+
const prevComment = buffer[buffer.length - 1] ?? '';
|
|
1425
1471
|
if (!uidIgnore || prevComment.indexOf(uidIgnore) === -1) {
|
|
1426
1472
|
if (!prevComment) {
|
|
1427
1473
|
prevTag = charsPrevTag;
|
|
@@ -1429,7 +1475,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1429
1475
|
}
|
|
1430
1476
|
if (buffer.length > 1 && (!prevComment || (!options.conservativeCollapse && / $/.test(currentChars)))) {
|
|
1431
1477
|
const charsIndex = buffer.length - 2;
|
|
1432
|
-
buffer[charsIndex] = buffer[charsIndex].replace(/\s+$/, function (trailingSpaces) {
|
|
1478
|
+
buffer[charsIndex] = (buffer[charsIndex] ?? '').replace(/\s+$/, function (trailingSpaces) {
|
|
1433
1479
|
text = trailingSpaces + text;
|
|
1434
1480
|
return '';
|
|
1435
1481
|
});
|
|
@@ -1440,13 +1486,14 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1440
1486
|
// when `nextTag` is `comment` (another UID placeholder), `commentFinalize` handles it
|
|
1441
1487
|
const match = prevComment.match(uidIgnorePlaceholderPattern);
|
|
1442
1488
|
if (match) {
|
|
1443
|
-
const idx = +match[1];
|
|
1489
|
+
const idx = +(match[1] ?? '');
|
|
1444
1490
|
if (idx < ignoredMarkupChunks.length) {
|
|
1445
1491
|
const content = ignoredMarkupChunks[idx];
|
|
1446
1492
|
const lastTagMatch = content && RE_LAST_HTML_TAG.exec(content);
|
|
1447
1493
|
if (lastTagMatch) {
|
|
1448
|
-
const
|
|
1449
|
-
const
|
|
1494
|
+
const group = lastTagMatch[1] ?? '';
|
|
1495
|
+
const isClose = group.charAt(0) === '/';
|
|
1496
|
+
const tagName = resolveName(isClose ? group.slice(1) : group);
|
|
1450
1497
|
effectivePrevTag = isClose ? '/' + tagName : tagName;
|
|
1451
1498
|
}
|
|
1452
1499
|
}
|
|
@@ -1457,13 +1504,13 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1457
1504
|
if (prevTag === '/nobr' || prevTag === 'wbr') {
|
|
1458
1505
|
if (/^\s/.test(text)) {
|
|
1459
1506
|
let tagIndex = buffer.length - 1;
|
|
1460
|
-
while (tagIndex > 0 && buffer[tagIndex].lastIndexOf('<' + prevTag) !== 0) {
|
|
1507
|
+
while (tagIndex > 0 && (buffer[tagIndex] ?? '').lastIndexOf('<' + prevTag) !== 0) {
|
|
1461
1508
|
tagIndex--;
|
|
1462
1509
|
}
|
|
1463
1510
|
trimTrailingWhitespace(tagIndex - 1, 'br');
|
|
1464
1511
|
}
|
|
1465
1512
|
} else if (inlineTextSet.has(prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag)) {
|
|
1466
|
-
text = collapseWhitespace(text, options, /(?:^|\s)$/.test(currentChars));
|
|
1513
|
+
text = collapseWhitespace(text, options, /(?:^|\s)$/.test(currentChars), false);
|
|
1467
1514
|
}
|
|
1468
1515
|
}
|
|
1469
1516
|
if (prevTag || nextTag) {
|
|
@@ -1483,18 +1530,18 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1483
1530
|
}
|
|
1484
1531
|
|
|
1485
1532
|
// Finalization phase (sync): Optional tag handling, entity re-encoding, buffer push
|
|
1486
|
-
function charsFinalize(text) {
|
|
1533
|
+
function charsFinalize(/** @type {string} */ text) {
|
|
1487
1534
|
if (options.removeOptionalTags && text) {
|
|
1488
1535
|
// UID-attr tokens are padded with `\t`, which would falsely look like leading whitespace;
|
|
1489
1536
|
// resolve single-token text to its actual content for the space/comment checks below
|
|
1490
1537
|
let effectiveText = text;
|
|
1491
|
-
if (uidAttrLeadingPattern && text.includes(uidAttr)) {
|
|
1538
|
+
if (uidAttrLeadingPattern && uidAttr && text.includes(uidAttr)) {
|
|
1492
1539
|
const uidMatch = uidAttrLeadingPattern.exec(text);
|
|
1493
1540
|
if (uidMatch) {
|
|
1494
|
-
const idx = +uidMatch[1];
|
|
1541
|
+
const idx = +(uidMatch[1] ?? 0);
|
|
1495
1542
|
const chunks = idx < ignoredCustomMarkupChunks.length ? ignoredCustomMarkupChunks[idx] : null;
|
|
1496
1543
|
if (chunks != null) {
|
|
1497
|
-
effectiveText = chunks[0];
|
|
1544
|
+
effectiveText = chunks[0] ?? '';
|
|
1498
1545
|
}
|
|
1499
1546
|
}
|
|
1500
1547
|
}
|
|
@@ -1530,8 +1577,8 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1530
1577
|
}
|
|
1531
1578
|
}
|
|
1532
1579
|
if (uidPattern && options.collapseWhitespace && stackNoTrimWhitespace.length) {
|
|
1533
|
-
text = text.replace(uidPattern, function (match,
|
|
1534
|
-
return ignoredCustomMarkupChunks[+index][0];
|
|
1580
|
+
text = text.replace(/** @type {RegExp} */ (uidPattern), function (/** @type {string} */ match, /** @type {string} */ _prefix, /** @type {string} */ index) {
|
|
1581
|
+
return ignoredCustomMarkupChunks[+index]?.[0] ?? match;
|
|
1535
1582
|
});
|
|
1536
1583
|
}
|
|
1537
1584
|
currentChars += text;
|
|
@@ -1557,20 +1604,20 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1557
1604
|
text = await processScript(text, options, currentAttrs, minifyHTML);
|
|
1558
1605
|
}
|
|
1559
1606
|
if (needsMinifyJS) {
|
|
1560
|
-
text = await options.minifyJS(text, false, isModuleScript);
|
|
1607
|
+
text = await /** @type {Function} */ (options.minifyJS)(text, false, isModuleScript);
|
|
1561
1608
|
}
|
|
1562
1609
|
if (needsMinifyCSS) {
|
|
1563
|
-
text = await options.minifyCSS(text);
|
|
1610
|
+
text = await /** @type {Function} */ (options.minifyCSS)(text);
|
|
1564
1611
|
}
|
|
1565
1612
|
charsFinalize(text);
|
|
1566
1613
|
})();
|
|
1567
1614
|
},
|
|
1568
|
-
comment: function (text, nonStandard) {
|
|
1615
|
+
comment: function (/** @type {string} */ text, /** @type {boolean} */ nonStandard) {
|
|
1569
1616
|
const prefix = nonStandard ? '<!' : '<!--';
|
|
1570
1617
|
const suffix = nonStandard ? '>' : '-->';
|
|
1571
1618
|
|
|
1572
1619
|
// Finalization phase (sync): Optional tag handling, `htmlmin:ignore` whitespace collapsing, buffer push
|
|
1573
|
-
function commentFinalize(comment) {
|
|
1620
|
+
function commentFinalize(/** @type {string} */ comment) {
|
|
1574
1621
|
if (options.removeOptionalTags && comment) {
|
|
1575
1622
|
if (uidIgnorePlaceholderPattern) {
|
|
1576
1623
|
const match = uidIgnorePlaceholderPattern.exec(comment);
|
|
@@ -1579,11 +1626,12 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1579
1626
|
// if there’s a pending optional end tag and the ignored content isn’t itself
|
|
1580
1627
|
// a comment (which per the HTML spec prevents omission), resolve it now,
|
|
1581
1628
|
// before the UID is pushed to the buffer
|
|
1582
|
-
const idx = +match[1];
|
|
1629
|
+
const idx = +(match[1] ?? 0);
|
|
1583
1630
|
const content = idx < ignoredMarkupChunks.length ? ignoredMarkupChunks[idx] : null;
|
|
1584
1631
|
if (optionalEndTag && optionalEndTagEmitted && content != null && !/^\s*<!--/.test(content)) {
|
|
1585
1632
|
const firstTagMatch = content.match(/^\s*<([a-zA-Z][^\s/>]*)/);
|
|
1586
|
-
const
|
|
1633
|
+
const firstTagGroup = firstTagMatch?.[1] ?? '';
|
|
1634
|
+
const firstTag = firstTagGroup ? resolveName(firstTagGroup) : '';
|
|
1587
1635
|
if (canRemovePrecedingTag(optionalEndTag, firstTag)) {
|
|
1588
1636
|
removeEndTag();
|
|
1589
1637
|
}
|
|
@@ -1611,8 +1659,8 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1611
1659
|
const prevMatch = prevComment.match(uidIgnorePlaceholderPattern);
|
|
1612
1660
|
|
|
1613
1661
|
if (currentMatch && prevMatch) {
|
|
1614
|
-
const currentIndex = +currentMatch[1];
|
|
1615
|
-
const prevIndex = +prevMatch[1];
|
|
1662
|
+
const currentIndex = +(currentMatch[1] ?? 0);
|
|
1663
|
+
const prevIndex = +(prevMatch[1] ?? 0);
|
|
1616
1664
|
|
|
1617
1665
|
// Defensive bounds check to ensure indices are valid
|
|
1618
1666
|
if (currentIndex < ignoredMarkupChunks.length && prevIndex < ignoredMarkupChunks.length) {
|
|
@@ -1635,13 +1683,13 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1635
1683
|
// Collapse if both sides are element/closing tags or HTML comments, and neither is inline
|
|
1636
1684
|
if ((currentTagMatch || currentIsHtmlComment || currentClosingTagMatch) &&
|
|
1637
1685
|
(prevTagMatch || prevIsHtmlComment || prevClosingTagMatch)) {
|
|
1638
|
-
const currentTag = currentTagMatch ?
|
|
1639
|
-
: currentClosingTagMatch ?
|
|
1640
|
-
const prevTag = prevTagMatch ?
|
|
1641
|
-
: prevClosingTagMatch ?
|
|
1686
|
+
const currentTag = currentTagMatch ? resolveName(currentTagMatch[1] ?? '')
|
|
1687
|
+
: currentClosingTagMatch ? resolveName(currentClosingTagMatch[1] ?? '') : null;
|
|
1688
|
+
const prevTag = prevTagMatch ? resolveName(prevTagMatch[1] ?? '')
|
|
1689
|
+
: prevClosingTagMatch ? resolveName(prevClosingTagMatch[1] ?? '') : null;
|
|
1642
1690
|
|
|
1643
1691
|
// Don’t collapse between inline elements (HTML comments count as non-inline)
|
|
1644
|
-
if (!inlineElements.has(currentTag) && !inlineElements.has(prevTag)) {
|
|
1692
|
+
if (!inlineElements.has(currentTag ?? '') && !inlineElements.has(prevTag ?? '')) {
|
|
1645
1693
|
// Collapse whitespace respecting context rules
|
|
1646
1694
|
let collapsedText = prevText;
|
|
1647
1695
|
|
|
@@ -1676,7 +1724,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1676
1724
|
}
|
|
1677
1725
|
|
|
1678
1726
|
if (options.removeComments) {
|
|
1679
|
-
if (isIgnoredComment(text, options)) {
|
|
1727
|
+
if (isIgnoredComment(text, /** @type {any} */ (options))) {
|
|
1680
1728
|
text = prefix + text + suffix;
|
|
1681
1729
|
} else {
|
|
1682
1730
|
text = '';
|
|
@@ -1686,7 +1734,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1686
1734
|
}
|
|
1687
1735
|
commentFinalize(text);
|
|
1688
1736
|
},
|
|
1689
|
-
doctype: function (doctype) {
|
|
1737
|
+
doctype: function (/** @type {string} */ doctype) {
|
|
1690
1738
|
buffer.push(options.useShortDoctype
|
|
1691
1739
|
? '<!doctype' +
|
|
1692
1740
|
(options.removeTagWhitespace ? '' : ' ') + 'html>'
|
|
@@ -1701,11 +1749,12 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1701
1749
|
if (options.minifySVG && svgBlocks.length) {
|
|
1702
1750
|
const optimized = await Promise.all(
|
|
1703
1751
|
svgBlocks.map(({ start, end }) =>
|
|
1704
|
-
options.minifySVG(buffer.slice(start, end).join(''))
|
|
1752
|
+
/** @type {Function} */ (options.minifySVG)(buffer.slice(start, end).join(''))
|
|
1705
1753
|
)
|
|
1706
1754
|
);
|
|
1707
1755
|
for (let i = svgBlocks.length - 1; i >= 0; i--) {
|
|
1708
|
-
|
|
1756
|
+
const block = svgBlocks[i];
|
|
1757
|
+
if (block) buffer.splice(block.start, block.end - block.start, optimized[i] ?? '');
|
|
1709
1758
|
}
|
|
1710
1759
|
}
|
|
1711
1760
|
|
|
@@ -1725,9 +1774,9 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1725
1774
|
}
|
|
1726
1775
|
|
|
1727
1776
|
return joinResultSegments(buffer, options, uidPattern
|
|
1728
|
-
? function (str) {
|
|
1729
|
-
return str.replace(uidPattern, function (match, prefix, index, suffix) {
|
|
1730
|
-
let chunk = ignoredCustomMarkupChunks[+index][0];
|
|
1777
|
+
? function (/** @type {string} */ str) {
|
|
1778
|
+
return str.replace(/** @type {RegExp} */ (uidPattern), function (/** @type {string} */ match, /** @type {string} */ prefix, /** @type {string} */ index, /** @type {string} */ suffix) {
|
|
1779
|
+
let chunk = ignoredCustomMarkupChunks[+index]?.[0] ?? match;
|
|
1731
1780
|
if (options.collapseWhitespace) {
|
|
1732
1781
|
if (prefix !== '\t') {
|
|
1733
1782
|
chunk = prefix + chunk;
|
|
@@ -1744,14 +1793,20 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1744
1793
|
});
|
|
1745
1794
|
}
|
|
1746
1795
|
: identity, uidIgnore
|
|
1747
|
-
? function (str) {
|
|
1748
|
-
return str.replace(new RegExp('<!--' + uidIgnore + '([0-9]+)-->', 'g'), function (
|
|
1749
|
-
return ignoredMarkupChunks[+index];
|
|
1796
|
+
? function (/** @type {string} */ str) {
|
|
1797
|
+
return str.replace(new RegExp('<!--' + uidIgnore + '([0-9]+)-->', 'g'), function (/** @type {string} */ _match, /** @type {string} */ index) {
|
|
1798
|
+
return ignoredMarkupChunks[+index] ?? '';
|
|
1750
1799
|
});
|
|
1751
1800
|
}
|
|
1752
1801
|
: identity);
|
|
1753
1802
|
}
|
|
1754
1803
|
|
|
1804
|
+
/**
|
|
1805
|
+
* @param {string[]} results
|
|
1806
|
+
* @param {MinifierOptions} options
|
|
1807
|
+
* @param {Function} restoreCustom
|
|
1808
|
+
* @param {Function} restoreIgnore
|
|
1809
|
+
*/
|
|
1755
1810
|
function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
|
|
1756
1811
|
let str;
|
|
1757
1812
|
const maxLineLength = options.maxLineLength;
|
|
@@ -1761,15 +1816,16 @@ function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
|
|
|
1761
1816
|
let line = ''; const lines = [];
|
|
1762
1817
|
while (results.length) {
|
|
1763
1818
|
const len = line.length;
|
|
1764
|
-
const
|
|
1765
|
-
const
|
|
1819
|
+
const cur = results[0] ?? '';
|
|
1820
|
+
const end = cur.indexOf('\n');
|
|
1821
|
+
const isClosingTag = Boolean(cur.match(endTag));
|
|
1766
1822
|
const shouldKeepSameLine = noNewlinesBeforeTagClose && isClosingTag;
|
|
1767
1823
|
|
|
1768
1824
|
if (end < 0) {
|
|
1769
|
-
line += restoreIgnore(restoreCustom(results.shift()));
|
|
1825
|
+
line += restoreIgnore(restoreCustom(results.shift() ?? ''));
|
|
1770
1826
|
} else {
|
|
1771
|
-
line += restoreIgnore(restoreCustom(
|
|
1772
|
-
results[0] =
|
|
1827
|
+
line += restoreIgnore(restoreCustom(cur.slice(0, end)));
|
|
1828
|
+
results[0] = cur.slice(end + 1);
|
|
1773
1829
|
}
|
|
1774
1830
|
if (len > 0 && line.length > maxLineLength && !shouldKeepSameLine) {
|
|
1775
1831
|
lines.push(line.slice(0, len));
|
|
@@ -1799,13 +1855,14 @@ function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
|
|
|
1799
1855
|
* - The first call’s options determine the cache sizes for subsequent calls
|
|
1800
1856
|
* - Invalid values (NaN, Infinity) fall back to the default size (500); values below `1` are clamped to `1`
|
|
1801
1857
|
*/
|
|
1858
|
+
/** @param {MinifierOptions} options */
|
|
1802
1859
|
function initCaches(options) {
|
|
1803
1860
|
// Only create caches once (on first call)—sizes are locked after this
|
|
1804
1861
|
if (!cssMinifyCache) {
|
|
1805
1862
|
const defaultSize = 500;
|
|
1806
1863
|
|
|
1807
1864
|
// Helper to parse env var—returns parsed number (including 0) or undefined if absent, invalid, or negative
|
|
1808
|
-
const parseEnvCacheSize = (envVar) => {
|
|
1865
|
+
const parseEnvCacheSize = (/** @type {string | undefined} */ envVar) => {
|
|
1809
1866
|
if (envVar === undefined) return undefined;
|
|
1810
1867
|
const parsed = Number(envVar);
|
|
1811
1868
|
if (Number.isNaN(parsed) || !Number.isFinite(parsed) || parsed < 0) {
|
|
@@ -1815,7 +1872,7 @@ function initCaches(options) {
|
|
|
1815
1872
|
};
|
|
1816
1873
|
|
|
1817
1874
|
// Sanitize a cache size: Non-finite/NaN falls back to `defaultSize`; otherwise clamped to min 1 and floored
|
|
1818
|
-
const sanitizeSize = (size) => Number.isFinite(size) ? Math.max(1, Math.floor(size)) : defaultSize;
|
|
1875
|
+
const sanitizeSize = (/** @type {number} */ size) => Number.isFinite(size) ? Math.max(1, Math.floor(size)) : defaultSize;
|
|
1819
1876
|
|
|
1820
1877
|
// Get cache sizes with precedence: Options > env > default
|
|
1821
1878
|
const cssSize = options.cacheCSS !== undefined ? options.cacheCSS
|
|
@@ -1853,7 +1910,9 @@ export const minify = async function (value, options) {
|
|
|
1853
1910
|
getTerser,
|
|
1854
1911
|
getSwc,
|
|
1855
1912
|
getSvgo,
|
|
1856
|
-
|
|
1913
|
+
cssMinifyCache: caches.cssMinifyCache ?? undefined,
|
|
1914
|
+
jsMinifyCache: caches.jsMinifyCache ?? undefined,
|
|
1915
|
+
svgMinifyCache: caches.svgMinifyCache ?? undefined
|
|
1857
1916
|
});
|
|
1858
1917
|
let result = await minifyHTML(value, options);
|
|
1859
1918
|
|
|
@@ -1862,7 +1921,7 @@ export const minify = async function (value, options) {
|
|
|
1862
1921
|
result = mergeConsecutiveScripts(result);
|
|
1863
1922
|
}
|
|
1864
1923
|
|
|
1865
|
-
options.log('minified in: ' + (Date.now() - start) + 'ms');
|
|
1924
|
+
/** @type {Function} */ (options.log)('minified in: ' + (Date.now() - start) + 'ms');
|
|
1866
1925
|
return result;
|
|
1867
1926
|
};
|
|
1868
1927
|
|