html-minifier-next 4.9.1 → 4.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -19
- package/dist/htmlminifier.cjs +191 -94
- package/dist/htmlminifier.esm.bundle.js +191 -94
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/htmlparser.d.ts.map +1 -1
- package/dist/types/tokenchain.d.ts +1 -0
- package/dist/types/tokenchain.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/htmlminifier.js +5 -1
- package/src/htmlparser.js +89 -50
- package/src/tokenchain.js +77 -34
package/README.md
CHANGED
|
@@ -112,6 +112,8 @@ HTML Minifier Next provides presets for common use cases. Presets are pre-config
|
|
|
112
112
|
* `conservative`: Safe minification suitable for most projects. Includes whitespace collapsing, comment removal, and doctype normalization.
|
|
113
113
|
* `comprehensive`: Aggressive minification for maximum file size reduction. Includes relevant conservative options plus attribute quote removal, optional tag removal, and more.
|
|
114
114
|
|
|
115
|
+
To review the specific options set, [presets.js](https://github.com/j9t/html-minifier-next/blob/main/src/presets.js) lists them in an accessible manner.
|
|
116
|
+
|
|
115
117
|
**Using presets:**
|
|
116
118
|
|
|
117
119
|
```bash
|
|
@@ -233,30 +235,34 @@ How does HTML Minifier Next compare to other minifiers? (All with the most aggre
|
|
|
233
235
|
| Site | Original Size (KB) | [HTML Minifier Next](https://github.com/j9t/html-minifier-next)<br>[](https://socket.dev/npm/package/html-minifier-next) | [HTML Minifier Terser](https://github.com/terser/html-minifier-terser)<br>[](https://socket.dev/npm/package/html-minifier-terser) | [htmlnano](https://github.com/posthtml/htmlnano)<br>[](https://socket.dev/npm/package/htmlnano) | [@swc/html](https://github.com/swc-project/swc)<br>[](https://socket.dev/npm/package/@swc/html) | [minify-html](https://github.com/wilsonzlin/minify-html)<br>[](https://socket.dev/npm/package/@minify-html/node) | [minimize](https://github.com/Swaagie/minimize)<br>[](https://socket.dev/npm/package/minimize) | [htmlcompressor.com](https://htmlcompressor.com/) |
|
|
234
236
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
235
237
|
| [A List Apart](https://alistapart.com/) | 59 | **49** | 50 | 51 | 52 | 51 | 54 | 52 |
|
|
236
|
-
| [Apple](https://www.apple.com/) | 260 | **
|
|
237
|
-
| [BBC](https://www.bbc.co.uk/) |
|
|
238
|
-
| [
|
|
238
|
+
| [Apple](https://www.apple.com/) | 260 | **203** | 204 | 231 | 235 | 236 | 238 | 238 |
|
|
239
|
+
| [BBC](https://www.bbc.co.uk/) | 720 | **655** | 665 | 677 | 677 | 678 | 714 | n/a |
|
|
240
|
+
| [CERN](https://home.cern/) | 156 | **87** | **87** | 95 | 95 | 95 | 97 | 100 |
|
|
241
|
+
| [CSS-Tricks](https://css-tricks.com/) | 163 | 122 | **120** | 128 | 143 | 144 | 149 | 145 |
|
|
239
242
|
| [ECMAScript](https://tc39.es/ecma262/) | 7238 | **6341** | **6341** | 6561 | 6444 | 6567 | 6614 | n/a |
|
|
240
|
-
| [
|
|
241
|
-
| [
|
|
242
|
-
| [
|
|
243
|
+
| [EDRi](https://edri.org/) | 80 | **59** | 60 | 70 | 70 | 71 | 75 | 73 |
|
|
244
|
+
| [EFF](https://www.eff.org/) | 56 | **48** | **48** | 50 | 49 | 49 | 51 | 51 |
|
|
245
|
+
| [European Alternatives](https://european-alternatives.eu/) | 48 | **30** | **30** | 32 | 32 | 32 | 32 | 32 |
|
|
246
|
+
| [FAZ](https://www.faz.net/aktuell/) | 1562 | 1455 | 1460 | **1400** | 1487 | 1498 | 1509 | n/a |
|
|
247
|
+
| [French Tech](https://lafrenchtech.gouv.fr/) | 152 | **121** | 122 | 125 | 125 | 125 | 132 | 126 |
|
|
248
|
+
| [Frontend Dogma](https://frontenddogma.com/) | 222 | **213** | 215 | 236 | 221 | 222 | 241 | 222 |
|
|
243
249
|
| [Google](https://www.google.com/) | 18 | **17** | **17** | **17** | **17** | **17** | 18 | 18 |
|
|
244
|
-
| [Ground News](https://ground.news/) |
|
|
250
|
+
| [Ground News](https://ground.news/) | 2339 | **2052** | 2056 | 2151 | 2179 | 2182 | 2326 | n/a |
|
|
245
251
|
| [HTML Living Standard](https://html.spec.whatwg.org/multipage/) | 149 | **147** | **147** | 153 | **147** | 149 | 155 | 149 |
|
|
246
|
-
| [Igalia](https://www.igalia.com/) |
|
|
247
|
-
| [Leanpub](https://leanpub.com/) |
|
|
248
|
-
| [Mastodon](https://mastodon.social/explore) |
|
|
249
|
-
| [MDN](https://developer.mozilla.org/en-US/) |
|
|
250
|
-
| [Middle East Eye](https://www.middleeasteye.net/) |
|
|
251
|
-
| [Nielsen Norman Group](https://www.nngroup.com/) | 84 |
|
|
252
|
-
| [SitePoint](https://www.sitepoint.com/) |
|
|
252
|
+
| [Igalia](https://www.igalia.com/) | 50 | **33** | **33** | 36 | 36 | 36 | 37 | 36 |
|
|
253
|
+
| [Leanpub](https://leanpub.com/) | 1204 | **997** | **997** | 1004 | 1003 | 1001 | 1198 | n/a |
|
|
254
|
+
| [Mastodon](https://mastodon.social/explore) | 37 | **27** | **27** | 32 | 34 | 35 | 35 | 35 |
|
|
255
|
+
| [MDN](https://developer.mozilla.org/en-US/) | 109 | **62** | **62** | 64 | 65 | 65 | 68 | 68 |
|
|
256
|
+
| [Middle East Eye](https://www.middleeasteye.net/) | 222 | **195** | 196 | 202 | 200 | 200 | 202 | 203 |
|
|
257
|
+
| [Nielsen Norman Group](https://www.nngroup.com/) | 84 | 71 | 71 | **53** | 71 | 73 | 74 | 73 |
|
|
258
|
+
| [SitePoint](https://www.sitepoint.com/) | 485 | **354** | **354** | 425 | 459 | 464 | 481 | n/a |
|
|
253
259
|
| [TetraLogical](https://tetralogical.com/) | 44 | 38 | 38 | **35** | 38 | 38 | 39 | 39 |
|
|
254
|
-
| [TPGi](https://www.tpgi.com/) | 175 | **
|
|
255
|
-
| [United Nations](https://www.un.org/en/) |
|
|
256
|
-
| [W3C](https://www.w3.org/) | 50 | **
|
|
257
|
-
| **Average processing time** | |
|
|
260
|
+
| [TPGi](https://www.tpgi.com/) | 175 | **159** | 161 | 160 | 164 | 165 | 172 | 171 |
|
|
261
|
+
| [United Nations](https://www.un.org/en/) | 151 | **112** | 114 | 121 | 125 | 124 | 130 | 123 |
|
|
262
|
+
| [W3C](https://www.w3.org/) | 50 | **36** | **36** | 38 | 38 | 38 | 40 | 38 |
|
|
263
|
+
| **Average processing time** | | 302 ms (26/26) | 356 ms (26/26) | 178 ms (26/26) | 59 ms (26/26) | **16 ms (26/26)** | 329 ms (26/26) | 1468 ms (20/26) |
|
|
258
264
|
|
|
259
|
-
(Last updated: Dec
|
|
265
|
+
(Last updated: Dec 16, 2025)
|
|
260
266
|
<!-- End auto-generated -->
|
|
261
267
|
|
|
262
268
|
## Examples
|
package/dist/htmlminifier.cjs
CHANGED
|
@@ -5,18 +5,6 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
5
5
|
var entities = require('entities');
|
|
6
6
|
var RelateURL = require('relateurl');
|
|
7
7
|
|
|
8
|
-
async function replaceAsync(str, regex, asyncFn) {
|
|
9
|
-
const promises = [];
|
|
10
|
-
|
|
11
|
-
str.replace(regex, (match, ...args) => {
|
|
12
|
-
const promise = asyncFn(match, ...args);
|
|
13
|
-
promises.push(promise);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
const data = await Promise.all(promises);
|
|
17
|
-
return str.replace(regex, () => data.shift());
|
|
18
|
-
}
|
|
19
|
-
|
|
20
8
|
/*!
|
|
21
9
|
* HTML Parser By John Resig (ejohn.org)
|
|
22
10
|
* Modified by Juriy “kangax” Zaytsev
|
|
@@ -24,6 +12,15 @@ async function replaceAsync(str, regex, asyncFn) {
|
|
|
24
12
|
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
|
|
25
13
|
*/
|
|
26
14
|
|
|
15
|
+
/*
|
|
16
|
+
* // Use like so:
|
|
17
|
+
* HTMLParser(htmlString, {
|
|
18
|
+
* start: function(tag, attrs, unary) {},
|
|
19
|
+
* end: function(tag) {},
|
|
20
|
+
* chars: function(text) {},
|
|
21
|
+
* comment: function(text) {}
|
|
22
|
+
* });
|
|
23
|
+
*/
|
|
27
24
|
|
|
28
25
|
class CaseInsensitiveSet extends Set {
|
|
29
26
|
has(str) {
|
|
@@ -92,6 +89,9 @@ const preCompiledStackedTags = {
|
|
|
92
89
|
'noscript': /([\s\S]*?)<\/noscript[^>]*>/i
|
|
93
90
|
};
|
|
94
91
|
|
|
92
|
+
// Cache for compiled attribute regexes per handler configuration
|
|
93
|
+
const attrRegexCache = new WeakMap();
|
|
94
|
+
|
|
95
95
|
function attrForHandler(handler) {
|
|
96
96
|
let pattern = singleAttrIdentifier.source +
|
|
97
97
|
'(?:\\s*(' + joinSingleAttrAssigns(handler) + ')' +
|
|
@@ -129,22 +129,47 @@ class HTMLParser {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
async parse() {
|
|
132
|
-
let html = this.html;
|
|
133
132
|
const handler = this.handler;
|
|
133
|
+
const fullHtml = this.html;
|
|
134
|
+
const fullLength = fullHtml.length;
|
|
134
135
|
|
|
135
136
|
const stack = []; let lastTag;
|
|
136
|
-
|
|
137
|
-
let
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
137
|
+
// Use cached attribute regex if available
|
|
138
|
+
let attribute = attrRegexCache.get(handler);
|
|
139
|
+
if (!attribute) {
|
|
140
|
+
attribute = attrForHandler(handler);
|
|
141
|
+
attrRegexCache.set(handler, attribute);
|
|
142
|
+
}
|
|
143
|
+
let prevTag = undefined, nextTag = undefined;
|
|
144
|
+
|
|
145
|
+
// Index-based parsing
|
|
146
|
+
let pos = 0;
|
|
147
|
+
let lastPos;
|
|
148
|
+
|
|
149
|
+
// Helper to get remaining HTML from current position
|
|
150
|
+
const remaining = () => fullHtml.slice(pos);
|
|
151
|
+
|
|
152
|
+
// Helper to advance position
|
|
153
|
+
const advance = (n) => { pos += n; };
|
|
154
|
+
|
|
155
|
+
// Lazy line/column calculation—only compute on actual errors
|
|
156
|
+
const getLineColumn = (position) => {
|
|
157
|
+
let line = 1;
|
|
158
|
+
let column = 1;
|
|
159
|
+
for (let i = 0; i < position; i++) {
|
|
160
|
+
if (fullHtml[i] === '\n') {
|
|
161
|
+
line++;
|
|
162
|
+
column = 1;
|
|
163
|
+
} else {
|
|
164
|
+
column++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { line, column };
|
|
144
168
|
};
|
|
145
169
|
|
|
146
|
-
while (
|
|
147
|
-
|
|
170
|
+
while (pos < fullLength) {
|
|
171
|
+
lastPos = pos;
|
|
172
|
+
const html = remaining();
|
|
148
173
|
// Make sure we’re not in a `script` or `style` element
|
|
149
174
|
if (!lastTag || !special.has(lastTag)) {
|
|
150
175
|
let textEnd = html.indexOf('<');
|
|
@@ -157,7 +182,7 @@ class HTMLParser {
|
|
|
157
182
|
if (handler.comment) {
|
|
158
183
|
await handler.comment(html.substring(4, commentEnd));
|
|
159
184
|
}
|
|
160
|
-
|
|
185
|
+
advance(commentEnd + 3);
|
|
161
186
|
prevTag = '';
|
|
162
187
|
continue;
|
|
163
188
|
}
|
|
@@ -171,7 +196,7 @@ class HTMLParser {
|
|
|
171
196
|
if (handler.comment) {
|
|
172
197
|
await handler.comment(html.substring(2, conditionalEnd + 1), true /* non-standard */);
|
|
173
198
|
}
|
|
174
|
-
|
|
199
|
+
advance(conditionalEnd + 2);
|
|
175
200
|
prevTag = '';
|
|
176
201
|
continue;
|
|
177
202
|
}
|
|
@@ -183,7 +208,7 @@ class HTMLParser {
|
|
|
183
208
|
if (handler.doctype) {
|
|
184
209
|
handler.doctype(doctypeMatch[0]);
|
|
185
210
|
}
|
|
186
|
-
|
|
211
|
+
advance(doctypeMatch[0].length);
|
|
187
212
|
prevTag = '';
|
|
188
213
|
continue;
|
|
189
214
|
}
|
|
@@ -191,8 +216,8 @@ class HTMLParser {
|
|
|
191
216
|
// End tag
|
|
192
217
|
const endTagMatch = html.match(endTag);
|
|
193
218
|
if (endTagMatch) {
|
|
194
|
-
|
|
195
|
-
await
|
|
219
|
+
advance(endTagMatch[0].length);
|
|
220
|
+
await parseEndTag(endTagMatch[0], endTagMatch[1]);
|
|
196
221
|
prevTag = '/' + endTagMatch[1].toLowerCase();
|
|
197
222
|
continue;
|
|
198
223
|
}
|
|
@@ -200,7 +225,7 @@ class HTMLParser {
|
|
|
200
225
|
// Start tag
|
|
201
226
|
const startTagMatch = parseStartTag(html);
|
|
202
227
|
if (startTagMatch) {
|
|
203
|
-
|
|
228
|
+
advance(startTagMatch.advance);
|
|
204
229
|
await handleStartTag(startTagMatch);
|
|
205
230
|
prevTag = startTagMatch.tagName.toLowerCase();
|
|
206
231
|
continue;
|
|
@@ -215,18 +240,19 @@ class HTMLParser {
|
|
|
215
240
|
let text;
|
|
216
241
|
if (textEnd >= 0) {
|
|
217
242
|
text = html.substring(0, textEnd);
|
|
218
|
-
|
|
243
|
+
advance(textEnd);
|
|
219
244
|
} else {
|
|
220
245
|
text = html;
|
|
221
|
-
html
|
|
246
|
+
advance(html.length);
|
|
222
247
|
}
|
|
223
248
|
|
|
224
249
|
// Next tag
|
|
225
|
-
|
|
250
|
+
const nextHtml = remaining();
|
|
251
|
+
let nextTagMatch = parseStartTag(nextHtml);
|
|
226
252
|
if (nextTagMatch) {
|
|
227
253
|
nextTag = nextTagMatch.tagName;
|
|
228
254
|
} else {
|
|
229
|
-
nextTagMatch =
|
|
255
|
+
nextTagMatch = nextHtml.match(endTag);
|
|
230
256
|
if (nextTagMatch) {
|
|
231
257
|
nextTag = '/' + nextTagMatch[1];
|
|
232
258
|
} else {
|
|
@@ -243,45 +269,50 @@ class HTMLParser {
|
|
|
243
269
|
// Use pre-compiled regex for common tags (`script`, `style`, `noscript`) to avoid regex creation overhead
|
|
244
270
|
const reStackedTag = preCompiledStackedTags[stackedTag] || reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)</' + stackedTag + '[^>]*>', 'i'));
|
|
245
271
|
|
|
246
|
-
|
|
272
|
+
const m = reStackedTag.exec(html);
|
|
273
|
+
if (m) {
|
|
274
|
+
let text = m[1];
|
|
247
275
|
if (stackedTag !== 'script' && stackedTag !== 'style' && stackedTag !== 'noscript') {
|
|
248
276
|
text = text
|
|
249
277
|
.replace(/<!--([\s\S]*?)-->/g, '$1')
|
|
250
278
|
.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1');
|
|
251
279
|
}
|
|
252
|
-
|
|
253
280
|
if (handler.chars) {
|
|
254
281
|
await handler.chars(text);
|
|
255
282
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
283
|
+
// Advance HTML past the matched special tag content and its closing tag
|
|
284
|
+
advance(m.index + m[0].length);
|
|
285
|
+
await parseEndTag('</' + stackedTag + '>', stackedTag);
|
|
286
|
+
} else {
|
|
287
|
+
// No closing tag found; to avoid infinite loop, break similarly to previous behavior
|
|
288
|
+
if (handler.continueOnParseError && handler.chars && html) {
|
|
289
|
+
await handler.chars(html[0], prevTag, '');
|
|
290
|
+
advance(1);
|
|
291
|
+
} else {
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
261
295
|
}
|
|
262
296
|
|
|
263
|
-
if (
|
|
297
|
+
if (pos === lastPos) {
|
|
264
298
|
if (handler.continueOnParseError) {
|
|
265
299
|
// Skip the problematic character and continue
|
|
266
300
|
if (handler.chars) {
|
|
267
|
-
await handler.chars(
|
|
301
|
+
await handler.chars(fullHtml[pos], prevTag, '');
|
|
268
302
|
}
|
|
269
|
-
|
|
270
|
-
position++;
|
|
303
|
+
advance(1);
|
|
271
304
|
prevTag = '';
|
|
272
305
|
continue;
|
|
273
306
|
}
|
|
274
|
-
const loc = getLineColumn(
|
|
275
|
-
// Include some context before the error position so the snippet contains
|
|
276
|
-
// the offending markup plus preceding characters (e.g. "invalid<tag").
|
|
307
|
+
const loc = getLineColumn(pos);
|
|
308
|
+
// Include some context before the error position so the snippet contains the offending markup plus preceding characters (e.g., “invalid<tag”)
|
|
277
309
|
const CONTEXT_BEFORE = 50;
|
|
278
|
-
const startPos = Math.max(0,
|
|
279
|
-
const snippet =
|
|
310
|
+
const startPos = Math.max(0, pos - CONTEXT_BEFORE);
|
|
311
|
+
const snippet = fullHtml.slice(startPos, startPos + 200).replace(/\n/g, ' ');
|
|
280
312
|
throw new Error(
|
|
281
|
-
`Parse error at line ${loc.line}, column ${loc.column}:\n${snippet}${
|
|
313
|
+
`Parse error at line ${loc.line}, column ${loc.column}:\n${snippet}${fullHtml.length > startPos + 200 ? '…' : ''}`
|
|
282
314
|
);
|
|
283
315
|
}
|
|
284
|
-
position = this.html.length - html.length;
|
|
285
316
|
}
|
|
286
317
|
|
|
287
318
|
if (!handler.partialMarkup) {
|
|
@@ -294,9 +325,11 @@ class HTMLParser {
|
|
|
294
325
|
if (start) {
|
|
295
326
|
const match = {
|
|
296
327
|
tagName: start[1],
|
|
297
|
-
attrs: []
|
|
328
|
+
attrs: [],
|
|
329
|
+
advance: 0
|
|
298
330
|
};
|
|
299
|
-
|
|
331
|
+
let consumed = start[0].length;
|
|
332
|
+
input = input.slice(consumed);
|
|
300
333
|
let end, attr;
|
|
301
334
|
|
|
302
335
|
// Safety limit: max length of input to check for attributes
|
|
@@ -346,7 +379,9 @@ class HTMLParser {
|
|
|
346
379
|
} else {
|
|
347
380
|
attr[baseIndex + 3] = value; // Single-quoted value
|
|
348
381
|
}
|
|
349
|
-
|
|
382
|
+
const attrLen = fullAttr.length;
|
|
383
|
+
input = input.slice(attrLen);
|
|
384
|
+
consumed += attrLen;
|
|
350
385
|
match.attrs.push(attr);
|
|
351
386
|
continue;
|
|
352
387
|
}
|
|
@@ -363,7 +398,9 @@ class HTMLParser {
|
|
|
363
398
|
break;
|
|
364
399
|
}
|
|
365
400
|
|
|
366
|
-
|
|
401
|
+
const attrLen = attr[0].length;
|
|
402
|
+
input = input.slice(attrLen);
|
|
403
|
+
consumed += attrLen;
|
|
367
404
|
match.attrs.push(attr);
|
|
368
405
|
}
|
|
369
406
|
|
|
@@ -371,7 +408,8 @@ class HTMLParser {
|
|
|
371
408
|
end = input.match(startTagClose);
|
|
372
409
|
if (end) {
|
|
373
410
|
match.unarySlash = end[1];
|
|
374
|
-
|
|
411
|
+
consumed += end[0].length;
|
|
412
|
+
match.advance = consumed;
|
|
375
413
|
return match;
|
|
376
414
|
}
|
|
377
415
|
}
|
|
@@ -381,7 +419,7 @@ class HTMLParser {
|
|
|
381
419
|
let pos;
|
|
382
420
|
const needle = tagName.toLowerCase();
|
|
383
421
|
for (pos = stack.length - 1; pos >= 0; pos--) {
|
|
384
|
-
const currentTag = stack[pos].
|
|
422
|
+
const currentTag = stack[pos].lowerTag;
|
|
385
423
|
if (currentTag === needle) {
|
|
386
424
|
return pos;
|
|
387
425
|
}
|
|
@@ -435,7 +473,7 @@ class HTMLParser {
|
|
|
435
473
|
}
|
|
436
474
|
if (tagName === 'col' && findTag('colgroup') < 0) {
|
|
437
475
|
lastTag = 'colgroup';
|
|
438
|
-
stack.push({ tag: lastTag, attrs: [] });
|
|
476
|
+
stack.push({ tag: lastTag, lowerTag: 'colgroup', attrs: [] });
|
|
439
477
|
if (handler.start) {
|
|
440
478
|
await handler.start(lastTag, [], false, '');
|
|
441
479
|
}
|
|
@@ -514,7 +552,7 @@ class HTMLParser {
|
|
|
514
552
|
});
|
|
515
553
|
|
|
516
554
|
if (!unary) {
|
|
517
|
-
stack.push({ tag: tagName, attrs });
|
|
555
|
+
stack.push({ tag: tagName, lowerTag: tagName.toLowerCase(), attrs });
|
|
518
556
|
lastTag = tagName;
|
|
519
557
|
unarySlash = '';
|
|
520
558
|
}
|
|
@@ -528,7 +566,7 @@ class HTMLParser {
|
|
|
528
566
|
let pos;
|
|
529
567
|
const needle = tagName.toLowerCase();
|
|
530
568
|
for (pos = stack.length - 1; pos >= 0; pos--) {
|
|
531
|
-
if (stack[pos].
|
|
569
|
+
if (stack[pos].lowerTag === needle) {
|
|
532
570
|
break;
|
|
533
571
|
}
|
|
534
572
|
}
|
|
@@ -580,21 +618,40 @@ class HTMLParser {
|
|
|
580
618
|
class Sorter {
|
|
581
619
|
sort(tokens, fromIndex = 0) {
|
|
582
620
|
for (let i = 0, len = this.keys.length; i < len; i++) {
|
|
583
|
-
const
|
|
584
|
-
|
|
621
|
+
const token = this.keys[i];
|
|
622
|
+
|
|
623
|
+
// Build position map for this token to avoid repeated `indexOf`
|
|
624
|
+
const positions = [];
|
|
625
|
+
for (let j = fromIndex; j < tokens.length; j++) {
|
|
626
|
+
if (tokens[j] === token) {
|
|
627
|
+
positions.push(j);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
585
630
|
|
|
586
|
-
|
|
631
|
+
if (positions.length > 0) {
|
|
632
|
+
// Build new array with tokens in sorted order instead of splicing
|
|
633
|
+
const result = [];
|
|
587
634
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
635
|
+
// Add all instances of the current token first
|
|
636
|
+
for (let j = 0; j < positions.length; j++) {
|
|
637
|
+
result.push(token);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Add other tokens, skipping positions where current token was
|
|
641
|
+
const posSet = new Set(positions);
|
|
642
|
+
for (let j = fromIndex; j < tokens.length; j++) {
|
|
643
|
+
if (!posSet.has(j)) {
|
|
644
|
+
result.push(tokens[j]);
|
|
593
645
|
}
|
|
594
|
-
|
|
595
|
-
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Copy sorted portion back to tokens array
|
|
649
|
+
for (let j = 0; j < result.length; j++) {
|
|
650
|
+
tokens[fromIndex + j] = result[j];
|
|
651
|
+
}
|
|
596
652
|
|
|
597
|
-
|
|
653
|
+
const newFromIndex = fromIndex + positions.length;
|
|
654
|
+
return this.sorterMap.get(token).sort(tokens, newFromIndex);
|
|
598
655
|
}
|
|
599
656
|
}
|
|
600
657
|
return tokens;
|
|
@@ -602,48 +659,84 @@ class Sorter {
|
|
|
602
659
|
}
|
|
603
660
|
|
|
604
661
|
class TokenChain {
|
|
662
|
+
constructor() {
|
|
663
|
+
// Use Map instead of object properties for better performance
|
|
664
|
+
this.map = new Map();
|
|
665
|
+
}
|
|
666
|
+
|
|
605
667
|
add(tokens) {
|
|
606
668
|
tokens.forEach((token) => {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
this[key] = [];
|
|
610
|
-
this[key].processed = 0;
|
|
669
|
+
if (!this.map.has(token)) {
|
|
670
|
+
this.map.set(token, { arrays: [], processed: 0 });
|
|
611
671
|
}
|
|
612
|
-
this
|
|
672
|
+
this.map.get(token).arrays.push(tokens);
|
|
613
673
|
});
|
|
614
674
|
}
|
|
615
675
|
|
|
616
676
|
createSorter() {
|
|
617
677
|
const sorter = new Sorter();
|
|
678
|
+
sorter.sorterMap = new Map();
|
|
679
|
+
|
|
680
|
+
// Convert Map entries to array and sort
|
|
681
|
+
const entries = Array.from(this.map.entries()).sort((a, b) => {
|
|
682
|
+
const m = a[1].arrays.length;
|
|
683
|
+
const n = b[1].arrays.length;
|
|
684
|
+
// Sort by length descending (larger first)
|
|
685
|
+
const lengthDiff = n - m;
|
|
686
|
+
if (lengthDiff !== 0) return lengthDiff;
|
|
687
|
+
// If lengths equal, sort by key ascending
|
|
688
|
+
return a[0].localeCompare(b[0]);
|
|
689
|
+
});
|
|
618
690
|
|
|
619
|
-
sorter.keys =
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}).filter((key) => {
|
|
624
|
-
if (this[key].processed < this[key].length) {
|
|
625
|
-
const token = key.slice(1);
|
|
691
|
+
sorter.keys = [];
|
|
692
|
+
|
|
693
|
+
entries.forEach(([token, data]) => {
|
|
694
|
+
if (data.processed < data.arrays.length) {
|
|
626
695
|
const chain = new TokenChain();
|
|
627
696
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
697
|
+
data.arrays.forEach((tokens) => {
|
|
698
|
+
// Build new array without the current token instead of splicing
|
|
699
|
+
const filtered = [];
|
|
700
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
701
|
+
if (tokens[i] !== token) {
|
|
702
|
+
filtered.push(tokens[i]);
|
|
703
|
+
}
|
|
632
704
|
}
|
|
633
|
-
|
|
634
|
-
|
|
705
|
+
|
|
706
|
+
// Mark remaining tokens as processed
|
|
707
|
+
filtered.forEach((t) => {
|
|
708
|
+
const tData = this.map.get(t);
|
|
709
|
+
if (tData) {
|
|
710
|
+
tData.processed++;
|
|
711
|
+
}
|
|
635
712
|
});
|
|
636
|
-
|
|
713
|
+
|
|
714
|
+
if (filtered.length > 0) {
|
|
715
|
+
chain.add(filtered);
|
|
716
|
+
}
|
|
637
717
|
});
|
|
638
|
-
|
|
639
|
-
|
|
718
|
+
|
|
719
|
+
sorter.keys.push(token);
|
|
720
|
+
sorter.sorterMap.set(token, chain.createSorter());
|
|
640
721
|
}
|
|
641
|
-
return false;
|
|
642
722
|
});
|
|
723
|
+
|
|
643
724
|
return sorter;
|
|
644
725
|
}
|
|
645
726
|
}
|
|
646
727
|
|
|
728
|
+
async function replaceAsync(str, regex, asyncFn) {
|
|
729
|
+
const promises = [];
|
|
730
|
+
|
|
731
|
+
str.replace(regex, (match, ...args) => {
|
|
732
|
+
const promise = asyncFn(match, ...args);
|
|
733
|
+
promises.push(promise);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
const data = await Promise.all(promises);
|
|
737
|
+
return str.replace(regex, () => data.shift());
|
|
738
|
+
}
|
|
739
|
+
|
|
647
740
|
/**
|
|
648
741
|
* Preset configurations for HTML Minifier Next
|
|
649
742
|
*
|
|
@@ -2294,7 +2387,9 @@ async function createSortFns(value, options, uidIgnore, uidAttr) {
|
|
|
2294
2387
|
currentType === 'text/html') {
|
|
2295
2388
|
await scan(text);
|
|
2296
2389
|
}
|
|
2297
|
-
}
|
|
2390
|
+
},
|
|
2391
|
+
// We never need `nextTag` information in this scan
|
|
2392
|
+
wantsNextTag: false
|
|
2298
2393
|
});
|
|
2299
2394
|
|
|
2300
2395
|
await parser.parse();
|
|
@@ -2530,6 +2625,8 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2530
2625
|
customAttrAssign: options.customAttrAssign,
|
|
2531
2626
|
customAttrSurround: options.customAttrSurround,
|
|
2532
2627
|
html5: options.html5,
|
|
2628
|
+
// Compute `nextTag` only when whitespace collapse features require it
|
|
2629
|
+
wantsNextTag: !!(options.collapseWhitespace || options.collapseInlineTagWhitespace || options.conservativeCollapse),
|
|
2533
2630
|
|
|
2534
2631
|
start: async function (tag, attrs, unary, unarySlash, autoGenerated) {
|
|
2535
2632
|
if (tag.toLowerCase() === 'svg') {
|