html-minifier-next 4.11.1 → 4.12.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 +17 -17
- package/cli.js +177 -68
- package/dist/htmlminifier.cjs +48 -17
- package/dist/htmlminifier.esm.bundle.js +48 -17
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/htmlparser.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/htmlminifier.js +37 -10
- package/src/htmlparser.js +11 -7
package/README.md
CHANGED
|
@@ -236,32 +236,32 @@ How does HTML Minifier Next compare to other minifiers? (All with the most aggre
|
|
|
236
236
|
| 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/) |
|
|
237
237
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
238
238
|
| [A List Apart](https://alistapart.com/) | 59 | **49** | 50 | 51 | 52 | 51 | 54 | 52 |
|
|
239
|
-
| [Apple](https://www.apple.com/) | 266 | **
|
|
240
|
-
| [BBC](https://www.bbc.co.uk/) |
|
|
241
|
-
| [CERN](https://home.cern/) | 156 | **87** |
|
|
242
|
-
| [CSS-Tricks](https://css-tricks.com/) |
|
|
243
|
-
| [ECMAScript](https://tc39.es/ecma262/) | 7238 | **
|
|
239
|
+
| [Apple](https://www.apple.com/) | 266 | **207** | **207** | 236 | 239 | 240 | 242 | 243 |
|
|
240
|
+
| [BBC](https://www.bbc.co.uk/) | 739 | **675** | 685 | 695 | 695 | 697 | 733 | n/a |
|
|
241
|
+
| [CERN](https://home.cern/) | 156 | **87** | **87** | 95 | 95 | 95 | 97 | 100 |
|
|
242
|
+
| [CSS-Tricks](https://css-tricks.com/) | 161 | 121 | **119** | 127 | 142 | 142 | 147 | 144 |
|
|
243
|
+
| [ECMAScript](https://tc39.es/ecma262/) | 7238 | **6341** | **6341** | 6561 | 6444 | 6567 | 6614 | n/a |
|
|
244
244
|
| [EDRi](https://edri.org/) | 80 | **59** | 60 | 70 | 70 | 71 | 75 | 73 |
|
|
245
|
-
| [EFF](https://www.eff.org/) | 55 | **47** |
|
|
245
|
+
| [EFF](https://www.eff.org/) | 55 | **47** | **47** | 50 | 48 | 49 | 50 | 50 |
|
|
246
246
|
| [European Alternatives](https://european-alternatives.eu/) | 48 | **30** | **30** | 32 | 32 | 32 | 32 | 32 |
|
|
247
|
-
| [FAZ](https://www.faz.net/aktuell/) |
|
|
247
|
+
| [FAZ](https://www.faz.net/aktuell/) | 1543 | 1436 | 1441 | **1383** | 1468 | 1479 | 1490 | n/a |
|
|
248
248
|
| [French Tech](https://lafrenchtech.gouv.fr/) | 152 | **121** | 122 | 125 | 125 | 125 | 132 | 126 |
|
|
249
249
|
| [Frontend Dogma](https://frontenddogma.com/) | 223 | **213** | 215 | 236 | 221 | 223 | 241 | 222 |
|
|
250
250
|
| [Google](https://www.google.com/) | 18 | **17** | **17** | **17** | **17** | **17** | 18 | 18 |
|
|
251
|
-
| [Ground News](https://ground.news/) |
|
|
252
|
-
| [HTML Living Standard](https://html.spec.whatwg.org/multipage/) | 149 | **147** |
|
|
253
|
-
| [Igalia](https://www.igalia.com/) | 50 | **
|
|
254
|
-
| [Leanpub](https://leanpub.com/) |
|
|
255
|
-
| [Mastodon](https://mastodon.social/explore) | 37 | **27** |
|
|
256
|
-
| [MDN](https://developer.mozilla.org/en-US/) | 109 | **62** |
|
|
257
|
-
| [Middle East Eye](https://www.middleeasteye.net/) |
|
|
251
|
+
| [Ground News](https://ground.news/) | 1437 | **1222** | 1225 | 1320 | 1346 | 1353 | 1424 | n/a |
|
|
252
|
+
| [HTML Living Standard](https://html.spec.whatwg.org/multipage/) | 149 | **147** | **147** | 153 | **147** | 149 | 155 | 149 |
|
|
253
|
+
| [Igalia](https://www.igalia.com/) | 50 | **34** | **34** | 36 | 36 | 36 | 37 | 37 |
|
|
254
|
+
| [Leanpub](https://leanpub.com/) | 1288 | **1092** | **1092** | 1099 | 1097 | 1094 | 1283 | n/a |
|
|
255
|
+
| [Mastodon](https://mastodon.social/explore) | 37 | **27** | **27** | 32 | 34 | 35 | 35 | 35 |
|
|
256
|
+
| [MDN](https://developer.mozilla.org/en-US/) | 109 | **62** | **62** | 64 | 65 | 65 | 68 | 68 |
|
|
257
|
+
| [Middle East Eye](https://www.middleeasteye.net/) | 223 | **196** | **196** | 203 | 201 | 200 | 202 | 203 |
|
|
258
258
|
| [Nielsen Norman Group](https://www.nngroup.com/) | 86 | 74 | 74 | **55** | 74 | 75 | 77 | 76 |
|
|
259
259
|
| [SitePoint](https://www.sitepoint.com/) | 485 | **354** | **354** | 425 | 459 | 464 | 481 | n/a |
|
|
260
260
|
| [TetraLogical](https://tetralogical.com/) | 44 | 38 | 38 | **35** | 38 | 38 | 39 | 39 |
|
|
261
|
-
| [TPGi](https://www.tpgi.com/) | 175 | **
|
|
261
|
+
| [TPGi](https://www.tpgi.com/) | 175 | **160** | 161 | **160** | 164 | 166 | 172 | 172 |
|
|
262
262
|
| [United Nations](https://www.un.org/en/) | 150 | **112** | 113 | 120 | 124 | 124 | 129 | 122 |
|
|
263
|
-
| [W3C](https://www.w3.org/) | 50 | **
|
|
264
|
-
| **Average processing time** | |
|
|
263
|
+
| [W3C](https://www.w3.org/) | 50 | **35** | 36 | 38 | 38 | 38 | 40 | 38 |
|
|
264
|
+
| **Average processing time** | | 306 ms (26/26) | 364 ms (26/26) | 162 ms (26/26) | 55 ms (26/26) | **16 ms (26/26)** | 316 ms (26/26) | 1439 ms (20/26) |
|
|
265
265
|
|
|
266
266
|
(Last updated: Dec 17, 2025)
|
|
267
267
|
<!-- End auto-generated -->
|
package/cli.js
CHANGED
|
@@ -28,10 +28,12 @@
|
|
|
28
28
|
import fs from 'fs';
|
|
29
29
|
import path from 'path';
|
|
30
30
|
import { pathToFileURL } from 'url';
|
|
31
|
+
import os from 'os';
|
|
31
32
|
import { createRequire } from 'module';
|
|
32
33
|
import { camelCase, paramCase } from 'change-case';
|
|
33
34
|
import { Command } from 'commander';
|
|
34
|
-
|
|
35
|
+
// Lazy-load HMN to reduce CLI cold-start overhead
|
|
36
|
+
// import { minify } from './src/htmlminifier.js';
|
|
35
37
|
import { getPreset, getPresetNames } from './src/presets.js';
|
|
36
38
|
|
|
37
39
|
const require = createRequire(import.meta.url);
|
|
@@ -182,6 +184,15 @@ program.option('-o --output <file>', 'Specify output file (reads from file argum
|
|
|
182
184
|
program.option('-v --verbose', 'Show detailed processing information');
|
|
183
185
|
program.option('-d --dry', 'Dry run: process and report statistics without writing output');
|
|
184
186
|
|
|
187
|
+
// Lazy import wrapper for HMN
|
|
188
|
+
let minifyFnPromise;
|
|
189
|
+
async function getMinify() {
|
|
190
|
+
if (!minifyFnPromise) {
|
|
191
|
+
minifyFnPromise = import('./src/htmlminifier.js').then(m => m.minify);
|
|
192
|
+
}
|
|
193
|
+
return minifyFnPromise;
|
|
194
|
+
}
|
|
195
|
+
|
|
185
196
|
function readFile(file) {
|
|
186
197
|
try {
|
|
187
198
|
return fs.readFileSync(file, { encoding: 'utf8' });
|
|
@@ -270,13 +281,30 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
270
281
|
(async () => {
|
|
271
282
|
let content;
|
|
272
283
|
let filesProvided = false;
|
|
284
|
+
let capturedFiles = [];
|
|
273
285
|
await program.arguments('[files...]').action(function (files) {
|
|
274
|
-
|
|
286
|
+
capturedFiles = files;
|
|
275
287
|
filesProvided = files.length > 0;
|
|
288
|
+
// Defer reading files until after we check for consumed filenames
|
|
276
289
|
}).parseAsync(process.argv);
|
|
277
290
|
|
|
278
291
|
const programOptions = program.opts();
|
|
279
292
|
|
|
293
|
+
// Check if any `parseJSON` options consumed a filename as their value
|
|
294
|
+
// If so, treat the option as boolean true and add the filename back to the files list
|
|
295
|
+
const jsonOptionKeys = ['minifyCss', 'minifyJs', 'minifyUrls'];
|
|
296
|
+
for (const key of jsonOptionKeys) {
|
|
297
|
+
const value = programOptions[key];
|
|
298
|
+
if (typeof value === 'string' && /\.(html?|php|xml|svg|xhtml|jsx|tsx|vue|ejs|hbs|mustache|twig)$/i.test(value)) {
|
|
299
|
+
// The option consumed a filename - inject it back
|
|
300
|
+
programOptions[key] = true;
|
|
301
|
+
capturedFiles.push(value);
|
|
302
|
+
filesProvided = true;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Defer reading files—multi-file mode will process per-file later
|
|
307
|
+
|
|
280
308
|
// Load and normalize config if `--config-file` was specified
|
|
281
309
|
if (programOptions.configFile) {
|
|
282
310
|
config = await loadConfigFromPath(programOptions.configFile);
|
|
@@ -344,6 +372,7 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
344
372
|
|
|
345
373
|
let minified;
|
|
346
374
|
try {
|
|
375
|
+
const minify = await getMinify();
|
|
347
376
|
minified = await minify(data, createOptions());
|
|
348
377
|
} catch (err) {
|
|
349
378
|
fatal('Minification error on ' + inputFile + '\n' + err.message);
|
|
@@ -472,6 +501,54 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
472
501
|
process.stderr.write('\r\x1b[K'); // Clear the line
|
|
473
502
|
}
|
|
474
503
|
|
|
504
|
+
// Utility: concurrency runner
|
|
505
|
+
async function runWithConcurrency(items, limit, worker) {
|
|
506
|
+
const results = new Array(items.length);
|
|
507
|
+
let next = 0;
|
|
508
|
+
let active = 0;
|
|
509
|
+
return new Promise((resolve, reject) => {
|
|
510
|
+
const launch = () => {
|
|
511
|
+
while (active < limit && next < items.length) {
|
|
512
|
+
const current = next++;
|
|
513
|
+
active++;
|
|
514
|
+
Promise.resolve(worker(items[current], current))
|
|
515
|
+
.then((res) => {
|
|
516
|
+
results[current] = res;
|
|
517
|
+
active--;
|
|
518
|
+
launch();
|
|
519
|
+
})
|
|
520
|
+
.catch(reject);
|
|
521
|
+
}
|
|
522
|
+
if (next >= items.length && active === 0) {
|
|
523
|
+
resolve(results);
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
launch();
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async function collectFiles(dir, extensions, skipRootAbs, ignorePatterns, baseDir) {
|
|
531
|
+
const out = [];
|
|
532
|
+
const entries = await fs.promises.readdir(dir).catch(() => []);
|
|
533
|
+
for (const name of entries) {
|
|
534
|
+
const filePath = path.join(dir, name);
|
|
535
|
+
if (skipRootAbs) {
|
|
536
|
+
const real = await fs.promises.realpath(filePath).catch(() => undefined);
|
|
537
|
+
if (real && (real === skipRootAbs || real.startsWith(skipRootAbs + path.sep))) continue;
|
|
538
|
+
}
|
|
539
|
+
const lst = await fs.promises.lstat(filePath).catch(() => null);
|
|
540
|
+
if (!lst || lst.isSymbolicLink()) continue;
|
|
541
|
+
if (lst.isDirectory()) {
|
|
542
|
+
if (shouldIgnoreDirectory(filePath, ignorePatterns, baseDir)) continue;
|
|
543
|
+
const sub = await collectFiles(filePath, extensions, skipRootAbs, ignorePatterns, baseDir);
|
|
544
|
+
out.push(...sub);
|
|
545
|
+
} else if (shouldProcessFile(name, extensions)) {
|
|
546
|
+
out.push(filePath);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return out;
|
|
550
|
+
}
|
|
551
|
+
|
|
475
552
|
async function processDirectory(inputDir, outputDir, extensions, isDryRun = false, isVerbose = false, skipRootAbs, progress = null, ignorePatterns = [], baseDir = null) {
|
|
476
553
|
// If first call provided a string, normalize once; otherwise assume pre-parsed array
|
|
477
554
|
if (typeof extensions === 'string') {
|
|
@@ -483,61 +560,27 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
483
560
|
baseDir = inputDir;
|
|
484
561
|
}
|
|
485
562
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
const real = await fs.promises.realpath(inputFile).catch(() => undefined);
|
|
499
|
-
if (real && (real === skipRootAbs || real.startsWith(skipRootAbs + path.sep))) {
|
|
500
|
-
continue;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
const lst = await fs.promises.lstat(inputFile).catch(err => {
|
|
505
|
-
fatal('Cannot read ' + inputFile + '\n' + err.message);
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
if (lst.isSymbolicLink()) {
|
|
509
|
-
continue;
|
|
563
|
+
// Collect all files first for bounded parallel processing
|
|
564
|
+
const list = await collectFiles(inputDir, extensions, skipRootAbs, ignorePatterns, baseDir);
|
|
565
|
+
const allStats = new Array(list.length);
|
|
566
|
+
const concurrency = Math.max(1, Math.min(os.cpus().length || 4, 8));
|
|
567
|
+
await runWithConcurrency(list, concurrency, async (inputFile, idx) => {
|
|
568
|
+
const rel = path.relative(inputDir, inputFile);
|
|
569
|
+
const outFile = path.join(outputDir, rel);
|
|
570
|
+
const outDir = path.dirname(outFile);
|
|
571
|
+
if (!isDryRun) {
|
|
572
|
+
await fs.promises.mkdir(outDir, { recursive: true }).catch(err => {
|
|
573
|
+
fatal('Cannot create directory ' + outDir + '\n' + err.message);
|
|
574
|
+
});
|
|
510
575
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}
|
|
517
|
-
const dirStats = await processDirectory(inputFile, outputFile, extensions, isDryRun, isVerbose, skipRootAbs, progress, ignorePatterns, baseDir);
|
|
518
|
-
if (dirStats) {
|
|
519
|
-
allStats.push(...dirStats);
|
|
520
|
-
}
|
|
521
|
-
} else if (shouldProcessFile(file, extensions)) {
|
|
522
|
-
if (!isDryRun) {
|
|
523
|
-
await fs.promises.mkdir(outputDir, { recursive: true }).catch(err => {
|
|
524
|
-
fatal('Cannot create directory ' + outputDir + '\n' + err.message);
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
const fileStats = await processFile(inputFile, outputFile, isDryRun, isVerbose);
|
|
528
|
-
if (fileStats) {
|
|
529
|
-
allStats.push(fileStats);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Update progress after processing
|
|
533
|
-
if (progress) {
|
|
534
|
-
progress.current++;
|
|
535
|
-
updateProgress(progress.current, progress.total);
|
|
536
|
-
}
|
|
576
|
+
const stats = await processFile(inputFile, outFile, isDryRun, isVerbose);
|
|
577
|
+
allStats[idx] = stats;
|
|
578
|
+
if (progress) {
|
|
579
|
+
progress.current++;
|
|
580
|
+
updateProgress(progress.current, progress.total);
|
|
537
581
|
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
return allStats;
|
|
582
|
+
});
|
|
583
|
+
return allStats.filter(Boolean);
|
|
541
584
|
}
|
|
542
585
|
|
|
543
586
|
const writeMinify = async () => {
|
|
@@ -551,6 +594,7 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
551
594
|
let minified;
|
|
552
595
|
|
|
553
596
|
try {
|
|
597
|
+
const minify = await getMinify();
|
|
554
598
|
minified = await minify(content, minifierOptions);
|
|
555
599
|
} catch (err) {
|
|
556
600
|
fatal('Minification error:\n' + err.message);
|
|
@@ -576,17 +620,12 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
576
620
|
}
|
|
577
621
|
|
|
578
622
|
if (programOptions.output) {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
.on('finish', resolve);
|
|
586
|
-
fileStream.end(minified);
|
|
587
|
-
}).catch((e) => {
|
|
588
|
-
fatal('Cannot write ' + programOptions.output + '\n' + e.message);
|
|
589
|
-
});
|
|
623
|
+
try {
|
|
624
|
+
await fs.promises.mkdir(path.dirname(programOptions.output), { recursive: true });
|
|
625
|
+
await fs.promises.writeFile(programOptions.output, minified, { encoding: 'utf8' });
|
|
626
|
+
} catch (err) {
|
|
627
|
+
fatal('Cannot write ' + programOptions.output + '\n' + err.message);
|
|
628
|
+
}
|
|
590
629
|
return;
|
|
591
630
|
}
|
|
592
631
|
|
|
@@ -646,6 +685,16 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
646
685
|
// Parse ignore patterns
|
|
647
686
|
const ignorePatterns = parseIgnorePatterns(resolvedIgnoreDir);
|
|
648
687
|
|
|
688
|
+
// Validate that the input directory exists and is readable
|
|
689
|
+
try {
|
|
690
|
+
const stat = await fs.promises.stat(inputDir);
|
|
691
|
+
if (!stat.isDirectory()) {
|
|
692
|
+
fatal(inputDir + ' is not a directory');
|
|
693
|
+
}
|
|
694
|
+
} catch (err) {
|
|
695
|
+
fatal('Cannot read directory ' + inputDir + '\n' + err.message);
|
|
696
|
+
}
|
|
697
|
+
|
|
649
698
|
// Resolve base directory for consistent path comparisons
|
|
650
699
|
const inputDirResolved = await fs.promises.realpath(inputDir).catch(() => inputDir);
|
|
651
700
|
|
|
@@ -688,12 +737,72 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
688
737
|
}
|
|
689
738
|
})();
|
|
690
739
|
} else if (filesProvided) { // Minifying one or more files specified on the CMD line
|
|
691
|
-
|
|
740
|
+
// Process each file independently, then concatenate outputs to preserve current behavior
|
|
741
|
+
const minifierOptions = createOptions();
|
|
742
|
+
// Show config info if verbose/dry
|
|
743
|
+
if (programOptions.verbose || programOptions.dry) {
|
|
744
|
+
getActiveOptionsDisplay(minifierOptions);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const concurrency = Math.max(1, Math.min(os.cpus().length || 4, 8));
|
|
748
|
+
const inputs = capturedFiles.slice();
|
|
749
|
+
|
|
750
|
+
// Read originals and minify in parallel with bounded concurrency
|
|
751
|
+
const originals = new Array(inputs.length);
|
|
752
|
+
const outputs = new Array(inputs.length);
|
|
753
|
+
|
|
754
|
+
await runWithConcurrency(inputs, concurrency, async (file, idx) => {
|
|
755
|
+
const data = await fs.promises.readFile(file, 'utf8').catch(err => fatal('Cannot read ' + file + '\n' + err.message));
|
|
756
|
+
const minify = await getMinify();
|
|
757
|
+
let out;
|
|
758
|
+
try {
|
|
759
|
+
out = await minify(data, minifierOptions);
|
|
760
|
+
} catch (err) {
|
|
761
|
+
fatal('Minification error on ' + file + '\n' + err.message);
|
|
762
|
+
}
|
|
763
|
+
originals[idx] = data;
|
|
764
|
+
outputs[idx] = out;
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
const originalCombined = originals.join('');
|
|
768
|
+
const minifiedCombined = outputs.join('');
|
|
769
|
+
|
|
770
|
+
const stats = calculateStats(originalCombined, minifiedCombined);
|
|
771
|
+
|
|
772
|
+
if (programOptions.dry) {
|
|
773
|
+
const inputSource = capturedFiles.join(', ');
|
|
774
|
+
const outputDest = programOptions.output || 'STDOUT';
|
|
775
|
+
console.error(`[DRY RUN] Would minify: ${inputSource} → ${outputDest}`);
|
|
776
|
+
console.error(` Original: ${stats.originalSize.toLocaleString()} bytes`);
|
|
777
|
+
console.error(` Minified: ${stats.minifiedSize.toLocaleString()} bytes`);
|
|
778
|
+
console.error(` Saved: ${stats.sign}${Math.abs(stats.saved).toLocaleString()} bytes (${stats.percentage}%)`);
|
|
779
|
+
process.exit(0);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if (programOptions.verbose) {
|
|
783
|
+
const inputSource = capturedFiles.join(', ');
|
|
784
|
+
console.error(` ✓ ${inputSource}: ${stats.originalSize.toLocaleString()} → ${stats.minifiedSize.toLocaleString()} bytes (${stats.sign}${Math.abs(stats.saved).toLocaleString()}, ${stats.percentage}%)`);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (programOptions.output) {
|
|
788
|
+
try {
|
|
789
|
+
await fs.promises.mkdir(path.dirname(programOptions.output), { recursive: true });
|
|
790
|
+
await fs.promises.writeFile(programOptions.output, minifiedCombined, 'utf8');
|
|
791
|
+
} catch (err) {
|
|
792
|
+
fatal('Cannot write ' + programOptions.output + '\n' + err.message);
|
|
793
|
+
}
|
|
794
|
+
} else {
|
|
795
|
+
process.stdout.write(minifiedCombined);
|
|
796
|
+
}
|
|
797
|
+
process.exit(0);
|
|
692
798
|
} else { // Minifying input coming from STDIN
|
|
693
799
|
content = '';
|
|
694
800
|
process.stdin.setEncoding('utf8');
|
|
695
801
|
process.stdin.on('data', function (data) {
|
|
696
802
|
content += data;
|
|
697
|
-
}).on('end',
|
|
803
|
+
}).on('end', async function() {
|
|
804
|
+
await writeMinify();
|
|
805
|
+
process.exit(0);
|
|
806
|
+
});
|
|
698
807
|
}
|
|
699
808
|
})();
|
package/dist/htmlminifier.cjs
CHANGED
|
@@ -92,7 +92,7 @@ const preCompiledStackedTags = {
|
|
|
92
92
|
// Cache for compiled attribute regexes per handler configuration
|
|
93
93
|
const attrRegexCache = new WeakMap();
|
|
94
94
|
|
|
95
|
-
function
|
|
95
|
+
function buildAttrRegex(handler) {
|
|
96
96
|
let pattern = singleAttrIdentifier.source +
|
|
97
97
|
'(?:\\s*(' + joinSingleAttrAssigns(handler) + ')' +
|
|
98
98
|
'[ \\t\\n\\f\\r]*(?:' + singleAttrValues.join('|') + '))?';
|
|
@@ -111,6 +111,14 @@ function attrForHandler(handler) {
|
|
|
111
111
|
return new RegExp('^\\s*' + pattern);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
function getAttrRegexForHandler(handler) {
|
|
115
|
+
let cached = attrRegexCache.get(handler);
|
|
116
|
+
if (cached) return cached;
|
|
117
|
+
const compiled = buildAttrRegex(handler);
|
|
118
|
+
attrRegexCache.set(handler, compiled);
|
|
119
|
+
return compiled;
|
|
120
|
+
}
|
|
121
|
+
|
|
114
122
|
function joinSingleAttrAssigns(handler) {
|
|
115
123
|
return singleAttrAssigns.concat(
|
|
116
124
|
handler.customAttrAssign || []
|
|
@@ -134,12 +142,8 @@ class HTMLParser {
|
|
|
134
142
|
const fullLength = fullHtml.length;
|
|
135
143
|
|
|
136
144
|
const stack = []; let lastTag;
|
|
137
|
-
// Use cached attribute regex
|
|
138
|
-
|
|
139
|
-
if (!attribute) {
|
|
140
|
-
attribute = attrForHandler(handler);
|
|
141
|
-
attrRegexCache.set(handler, attribute);
|
|
142
|
-
}
|
|
145
|
+
// Use cached attribute regex for this handler configuration
|
|
146
|
+
const attribute = getAttrRegexForHandler(handler);
|
|
143
147
|
let prevTag = undefined, nextTag = undefined;
|
|
144
148
|
|
|
145
149
|
// Index-based parsing
|
|
@@ -1263,7 +1267,7 @@ const cssMinifyCache = new LRU(200);
|
|
|
1263
1267
|
|
|
1264
1268
|
const trimWhitespace = str => {
|
|
1265
1269
|
if (!str) return str;
|
|
1266
|
-
// Fast path:
|
|
1270
|
+
// Fast path: If no whitespace at start or end, return early
|
|
1267
1271
|
if (!/^[ \n\r\t\f]/.test(str) && !/[ \n\r\t\f]$/.test(str)) {
|
|
1268
1272
|
return str;
|
|
1269
1273
|
}
|
|
@@ -1272,7 +1276,7 @@ const trimWhitespace = str => {
|
|
|
1272
1276
|
|
|
1273
1277
|
function collapseWhitespaceAll(str) {
|
|
1274
1278
|
if (!str) return str;
|
|
1275
|
-
// Fast path:
|
|
1279
|
+
// Fast path: If there are no common whitespace characters, return early
|
|
1276
1280
|
if (!/[ \n\r\t\f\xA0]/.test(str)) {
|
|
1277
1281
|
return str;
|
|
1278
1282
|
}
|
|
@@ -2027,7 +2031,7 @@ async function normalizeAttr(attr, attrs, tag, options) {
|
|
|
2027
2031
|
let attrValue = attr.value;
|
|
2028
2032
|
|
|
2029
2033
|
if (options.decodeEntities && attrValue) {
|
|
2030
|
-
// Fast path:
|
|
2034
|
+
// Fast path: Only decode when entities are present
|
|
2031
2035
|
if (attrValue.indexOf('&') !== -1) {
|
|
2032
2036
|
attrValue = entities.decodeHTMLStrict(attrValue);
|
|
2033
2037
|
}
|
|
@@ -2227,7 +2231,7 @@ const processOptions = (inputOptions) => {
|
|
|
2227
2231
|
const lightningCssOptions = typeof option === 'object' ? option : {};
|
|
2228
2232
|
|
|
2229
2233
|
options.minifyCSS = async function (text, type) {
|
|
2230
|
-
// Fast path:
|
|
2234
|
+
// Fast path: Nothing to minify
|
|
2231
2235
|
if (!text || !text.trim()) {
|
|
2232
2236
|
return text;
|
|
2233
2237
|
}
|
|
@@ -2319,7 +2323,7 @@ const processOptions = (inputOptions) => {
|
|
|
2319
2323
|
|
|
2320
2324
|
let jsKey;
|
|
2321
2325
|
try {
|
|
2322
|
-
// Fast path:
|
|
2326
|
+
// Fast path: Avoid invoking Terser for empty/whitespace-only content
|
|
2323
2327
|
if (!code || !code.trim()) {
|
|
2324
2328
|
return '';
|
|
2325
2329
|
}
|
|
@@ -2371,9 +2375,18 @@ const processOptions = (inputOptions) => {
|
|
|
2371
2375
|
relateUrlOptions = {};
|
|
2372
2376
|
}
|
|
2373
2377
|
|
|
2378
|
+
// Cache RelateURL instance for reuse (expensive to create)
|
|
2379
|
+
const relateUrlInstance = new RelateURL(relateUrlOptions.site || '', relateUrlOptions);
|
|
2380
|
+
|
|
2374
2381
|
options.minifyURLs = function (text) {
|
|
2382
|
+
// Fast-path: Skip if text doesn’t look like a URL that needs processing
|
|
2383
|
+
// Only process if contains URL-like characters (`/`, `:`, `#`, `?`) or spaces that need encoding
|
|
2384
|
+
if (!/[/:?#\s]/.test(text)) {
|
|
2385
|
+
return text;
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2375
2388
|
try {
|
|
2376
|
-
return
|
|
2389
|
+
return relateUrlInstance.relate(text);
|
|
2377
2390
|
} catch (err) {
|
|
2378
2391
|
if (!options.continueOnMinifyError) {
|
|
2379
2392
|
throw err;
|
|
@@ -2469,11 +2482,11 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
2469
2482
|
|
|
2470
2483
|
try {
|
|
2471
2484
|
await parser.parse();
|
|
2472
|
-
} catch (
|
|
2485
|
+
} catch (err) {
|
|
2473
2486
|
// If parsing fails during analysis pass, just skip it—we’ll still have
|
|
2474
2487
|
// partial frequency data from what we could parse
|
|
2475
2488
|
if (!options.continueOnParseError) {
|
|
2476
|
-
throw
|
|
2489
|
+
throw err;
|
|
2477
2490
|
}
|
|
2478
2491
|
}
|
|
2479
2492
|
}
|
|
@@ -2562,7 +2575,21 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
2562
2575
|
}
|
|
2563
2576
|
if (classChain) {
|
|
2564
2577
|
const sorter = classChain.createSorter();
|
|
2578
|
+
// Memoize `sortClassName` results—class lists often repeat in templates
|
|
2579
|
+
const classNameCache = new LRU(200);
|
|
2580
|
+
|
|
2565
2581
|
options.sortClassName = function (value) {
|
|
2582
|
+
// Fast path: Single class (no spaces) needs no sorting
|
|
2583
|
+
if (value.indexOf(' ') === -1) {
|
|
2584
|
+
return value;
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
// Check cache first
|
|
2588
|
+
const cached = classNameCache.get(value);
|
|
2589
|
+
if (cached !== undefined) {
|
|
2590
|
+
return cached;
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2566
2593
|
// Expand UID tokens back to original content before sorting
|
|
2567
2594
|
// Fast path: Skip if no HTML comments (UID markers) present
|
|
2568
2595
|
let expandedValue = value;
|
|
@@ -2577,7 +2604,11 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
2577
2604
|
return cls !== '';
|
|
2578
2605
|
});
|
|
2579
2606
|
const sorted = sorter.sort(classes);
|
|
2580
|
-
|
|
2607
|
+
const result = sorted.join(' ');
|
|
2608
|
+
|
|
2609
|
+
// Cache the result
|
|
2610
|
+
classNameCache.set(value, result);
|
|
2611
|
+
return result;
|
|
2581
2612
|
};
|
|
2582
2613
|
}
|
|
2583
2614
|
}
|
|
@@ -2618,7 +2649,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2618
2649
|
const customElementsInput = options.inlineCustomElements ?? [];
|
|
2619
2650
|
const customElementsArr = Array.isArray(customElementsInput) ? customElementsInput : Array.from(customElementsInput);
|
|
2620
2651
|
const normalizedCustomElements = customElementsArr.map(name => options.name(name));
|
|
2621
|
-
// Fast path:
|
|
2652
|
+
// Fast path: Reuse base Sets if no custom elements
|
|
2622
2653
|
const inlineTextSet = normalizedCustomElements.length
|
|
2623
2654
|
? new Set([...inlineElementsToKeepWhitespaceWithin, ...normalizedCustomElements])
|
|
2624
2655
|
: inlineElementsToKeepWhitespaceWithin;
|
|
@@ -5234,7 +5234,7 @@ const preCompiledStackedTags = {
|
|
|
5234
5234
|
// Cache for compiled attribute regexes per handler configuration
|
|
5235
5235
|
const attrRegexCache = new WeakMap();
|
|
5236
5236
|
|
|
5237
|
-
function
|
|
5237
|
+
function buildAttrRegex(handler) {
|
|
5238
5238
|
let pattern = singleAttrIdentifier.source +
|
|
5239
5239
|
'(?:\\s*(' + joinSingleAttrAssigns(handler) + ')' +
|
|
5240
5240
|
'[ \\t\\n\\f\\r]*(?:' + singleAttrValues.join('|') + '))?';
|
|
@@ -5253,6 +5253,14 @@ function attrForHandler(handler) {
|
|
|
5253
5253
|
return new RegExp('^\\s*' + pattern);
|
|
5254
5254
|
}
|
|
5255
5255
|
|
|
5256
|
+
function getAttrRegexForHandler(handler) {
|
|
5257
|
+
let cached = attrRegexCache.get(handler);
|
|
5258
|
+
if (cached) return cached;
|
|
5259
|
+
const compiled = buildAttrRegex(handler);
|
|
5260
|
+
attrRegexCache.set(handler, compiled);
|
|
5261
|
+
return compiled;
|
|
5262
|
+
}
|
|
5263
|
+
|
|
5256
5264
|
function joinSingleAttrAssigns(handler) {
|
|
5257
5265
|
return singleAttrAssigns.concat(
|
|
5258
5266
|
handler.customAttrAssign || []
|
|
@@ -5276,12 +5284,8 @@ class HTMLParser {
|
|
|
5276
5284
|
const fullLength = fullHtml.length;
|
|
5277
5285
|
|
|
5278
5286
|
const stack = []; let lastTag;
|
|
5279
|
-
// Use cached attribute regex
|
|
5280
|
-
|
|
5281
|
-
if (!attribute) {
|
|
5282
|
-
attribute = attrForHandler(handler);
|
|
5283
|
-
attrRegexCache.set(handler, attribute);
|
|
5284
|
-
}
|
|
5287
|
+
// Use cached attribute regex for this handler configuration
|
|
5288
|
+
const attribute = getAttrRegexForHandler(handler);
|
|
5285
5289
|
let prevTag = undefined, nextTag = undefined;
|
|
5286
5290
|
|
|
5287
5291
|
// Index-based parsing
|
|
@@ -6405,7 +6409,7 @@ const cssMinifyCache = new LRU(200);
|
|
|
6405
6409
|
|
|
6406
6410
|
const trimWhitespace = str => {
|
|
6407
6411
|
if (!str) return str;
|
|
6408
|
-
// Fast path:
|
|
6412
|
+
// Fast path: If no whitespace at start or end, return early
|
|
6409
6413
|
if (!/^[ \n\r\t\f]/.test(str) && !/[ \n\r\t\f]$/.test(str)) {
|
|
6410
6414
|
return str;
|
|
6411
6415
|
}
|
|
@@ -6414,7 +6418,7 @@ const trimWhitespace = str => {
|
|
|
6414
6418
|
|
|
6415
6419
|
function collapseWhitespaceAll(str) {
|
|
6416
6420
|
if (!str) return str;
|
|
6417
|
-
// Fast path:
|
|
6421
|
+
// Fast path: If there are no common whitespace characters, return early
|
|
6418
6422
|
if (!/[ \n\r\t\f\xA0]/.test(str)) {
|
|
6419
6423
|
return str;
|
|
6420
6424
|
}
|
|
@@ -7169,7 +7173,7 @@ async function normalizeAttr(attr, attrs, tag, options) {
|
|
|
7169
7173
|
let attrValue = attr.value;
|
|
7170
7174
|
|
|
7171
7175
|
if (options.decodeEntities && attrValue) {
|
|
7172
|
-
// Fast path:
|
|
7176
|
+
// Fast path: Only decode when entities are present
|
|
7173
7177
|
if (attrValue.indexOf('&') !== -1) {
|
|
7174
7178
|
attrValue = decodeHTMLStrict(attrValue);
|
|
7175
7179
|
}
|
|
@@ -7369,7 +7373,7 @@ const processOptions = (inputOptions) => {
|
|
|
7369
7373
|
const lightningCssOptions = typeof option === 'object' ? option : {};
|
|
7370
7374
|
|
|
7371
7375
|
options.minifyCSS = async function (text, type) {
|
|
7372
|
-
// Fast path:
|
|
7376
|
+
// Fast path: Nothing to minify
|
|
7373
7377
|
if (!text || !text.trim()) {
|
|
7374
7378
|
return text;
|
|
7375
7379
|
}
|
|
@@ -7461,7 +7465,7 @@ const processOptions = (inputOptions) => {
|
|
|
7461
7465
|
|
|
7462
7466
|
let jsKey;
|
|
7463
7467
|
try {
|
|
7464
|
-
// Fast path:
|
|
7468
|
+
// Fast path: Avoid invoking Terser for empty/whitespace-only content
|
|
7465
7469
|
if (!code || !code.trim()) {
|
|
7466
7470
|
return '';
|
|
7467
7471
|
}
|
|
@@ -7513,9 +7517,18 @@ const processOptions = (inputOptions) => {
|
|
|
7513
7517
|
relateUrlOptions = {};
|
|
7514
7518
|
}
|
|
7515
7519
|
|
|
7520
|
+
// Cache RelateURL instance for reuse (expensive to create)
|
|
7521
|
+
const relateUrlInstance = new RelateURL(relateUrlOptions.site || '', relateUrlOptions);
|
|
7522
|
+
|
|
7516
7523
|
options.minifyURLs = function (text) {
|
|
7524
|
+
// Fast-path: Skip if text doesn’t look like a URL that needs processing
|
|
7525
|
+
// Only process if contains URL-like characters (`/`, `:`, `#`, `?`) or spaces that need encoding
|
|
7526
|
+
if (!/[/:?#\s]/.test(text)) {
|
|
7527
|
+
return text;
|
|
7528
|
+
}
|
|
7529
|
+
|
|
7517
7530
|
try {
|
|
7518
|
-
return
|
|
7531
|
+
return relateUrlInstance.relate(text);
|
|
7519
7532
|
} catch (err) {
|
|
7520
7533
|
if (!options.continueOnMinifyError) {
|
|
7521
7534
|
throw err;
|
|
@@ -7611,11 +7624,11 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
7611
7624
|
|
|
7612
7625
|
try {
|
|
7613
7626
|
await parser.parse();
|
|
7614
|
-
} catch (
|
|
7627
|
+
} catch (err) {
|
|
7615
7628
|
// If parsing fails during analysis pass, just skip it—we’ll still have
|
|
7616
7629
|
// partial frequency data from what we could parse
|
|
7617
7630
|
if (!options.continueOnParseError) {
|
|
7618
|
-
throw
|
|
7631
|
+
throw err;
|
|
7619
7632
|
}
|
|
7620
7633
|
}
|
|
7621
7634
|
}
|
|
@@ -7704,7 +7717,21 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
7704
7717
|
}
|
|
7705
7718
|
if (classChain) {
|
|
7706
7719
|
const sorter = classChain.createSorter();
|
|
7720
|
+
// Memoize `sortClassName` results—class lists often repeat in templates
|
|
7721
|
+
const classNameCache = new LRU(200);
|
|
7722
|
+
|
|
7707
7723
|
options.sortClassName = function (value) {
|
|
7724
|
+
// Fast path: Single class (no spaces) needs no sorting
|
|
7725
|
+
if (value.indexOf(' ') === -1) {
|
|
7726
|
+
return value;
|
|
7727
|
+
}
|
|
7728
|
+
|
|
7729
|
+
// Check cache first
|
|
7730
|
+
const cached = classNameCache.get(value);
|
|
7731
|
+
if (cached !== undefined) {
|
|
7732
|
+
return cached;
|
|
7733
|
+
}
|
|
7734
|
+
|
|
7708
7735
|
// Expand UID tokens back to original content before sorting
|
|
7709
7736
|
// Fast path: Skip if no HTML comments (UID markers) present
|
|
7710
7737
|
let expandedValue = value;
|
|
@@ -7719,7 +7746,11 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
7719
7746
|
return cls !== '';
|
|
7720
7747
|
});
|
|
7721
7748
|
const sorted = sorter.sort(classes);
|
|
7722
|
-
|
|
7749
|
+
const result = sorted.join(' ');
|
|
7750
|
+
|
|
7751
|
+
// Cache the result
|
|
7752
|
+
classNameCache.set(value, result);
|
|
7753
|
+
return result;
|
|
7723
7754
|
};
|
|
7724
7755
|
}
|
|
7725
7756
|
}
|
|
@@ -7760,7 +7791,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
7760
7791
|
const customElementsInput = options.inlineCustomElements ?? [];
|
|
7761
7792
|
const customElementsArr = Array.isArray(customElementsInput) ? customElementsInput : Array.from(customElementsInput);
|
|
7762
7793
|
const normalizedCustomElements = customElementsArr.map(name => options.name(name));
|
|
7763
|
-
// Fast path:
|
|
7794
|
+
// Fast path: Reuse base Sets if no custom elements
|
|
7764
7795
|
const inlineTextSet = normalizedCustomElements.length
|
|
7765
7796
|
? new Set([...inlineElementsToKeepWhitespaceWithin, ...normalizedCustomElements])
|
|
7766
7797
|
: inlineElementsToKeepWhitespaceWithin;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"htmlparser.d.ts","sourceRoot":"","sources":["../../src/htmlparser.js"],"names":[],"mappings":"AA8CA,4BAAoE;
|
|
1
|
+
{"version":3,"file":"htmlparser.d.ts","sourceRoot":"","sources":["../../src/htmlparser.js"],"names":[],"mappings":"AA8CA,4BAAoE;AA+EpE;IACE,qCAGC;IAFC,UAAgB;IAChB,aAAsB;IAGxB,uBAgeC;CACF"}
|
package/package.json
CHANGED
package/src/htmlminifier.js
CHANGED
|
@@ -463,7 +463,7 @@ const cssMinifyCache = new LRU(200);
|
|
|
463
463
|
|
|
464
464
|
const trimWhitespace = str => {
|
|
465
465
|
if (!str) return str;
|
|
466
|
-
// Fast path:
|
|
466
|
+
// Fast path: If no whitespace at start or end, return early
|
|
467
467
|
if (!/^[ \n\r\t\f]/.test(str) && !/[ \n\r\t\f]$/.test(str)) {
|
|
468
468
|
return str;
|
|
469
469
|
}
|
|
@@ -472,7 +472,7 @@ const trimWhitespace = str => {
|
|
|
472
472
|
|
|
473
473
|
function collapseWhitespaceAll(str) {
|
|
474
474
|
if (!str) return str;
|
|
475
|
-
// Fast path:
|
|
475
|
+
// Fast path: If there are no common whitespace characters, return early
|
|
476
476
|
if (!/[ \n\r\t\f\xA0]/.test(str)) {
|
|
477
477
|
return str;
|
|
478
478
|
}
|
|
@@ -1227,7 +1227,7 @@ async function normalizeAttr(attr, attrs, tag, options) {
|
|
|
1227
1227
|
let attrValue = attr.value;
|
|
1228
1228
|
|
|
1229
1229
|
if (options.decodeEntities && attrValue) {
|
|
1230
|
-
// Fast path:
|
|
1230
|
+
// Fast path: Only decode when entities are present
|
|
1231
1231
|
if (attrValue.indexOf('&') !== -1) {
|
|
1232
1232
|
attrValue = decodeHTMLStrict(attrValue);
|
|
1233
1233
|
}
|
|
@@ -1427,7 +1427,7 @@ const processOptions = (inputOptions) => {
|
|
|
1427
1427
|
const lightningCssOptions = typeof option === 'object' ? option : {};
|
|
1428
1428
|
|
|
1429
1429
|
options.minifyCSS = async function (text, type) {
|
|
1430
|
-
// Fast path:
|
|
1430
|
+
// Fast path: Nothing to minify
|
|
1431
1431
|
if (!text || !text.trim()) {
|
|
1432
1432
|
return text;
|
|
1433
1433
|
}
|
|
@@ -1519,7 +1519,7 @@ const processOptions = (inputOptions) => {
|
|
|
1519
1519
|
|
|
1520
1520
|
let jsKey;
|
|
1521
1521
|
try {
|
|
1522
|
-
// Fast path:
|
|
1522
|
+
// Fast path: Avoid invoking Terser for empty/whitespace-only content
|
|
1523
1523
|
if (!code || !code.trim()) {
|
|
1524
1524
|
return '';
|
|
1525
1525
|
}
|
|
@@ -1571,9 +1571,18 @@ const processOptions = (inputOptions) => {
|
|
|
1571
1571
|
relateUrlOptions = {};
|
|
1572
1572
|
}
|
|
1573
1573
|
|
|
1574
|
+
// Cache RelateURL instance for reuse (expensive to create)
|
|
1575
|
+
const relateUrlInstance = new RelateURL(relateUrlOptions.site || '', relateUrlOptions);
|
|
1576
|
+
|
|
1574
1577
|
options.minifyURLs = function (text) {
|
|
1578
|
+
// Fast-path: Skip if text doesn’t look like a URL that needs processing
|
|
1579
|
+
// Only process if contains URL-like characters (`/`, `:`, `#`, `?`) or spaces that need encoding
|
|
1580
|
+
if (!/[/:?#\s]/.test(text)) {
|
|
1581
|
+
return text;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1575
1584
|
try {
|
|
1576
|
-
return
|
|
1585
|
+
return relateUrlInstance.relate(text);
|
|
1577
1586
|
} catch (err) {
|
|
1578
1587
|
if (!options.continueOnMinifyError) {
|
|
1579
1588
|
throw err;
|
|
@@ -1669,11 +1678,11 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
1669
1678
|
|
|
1670
1679
|
try {
|
|
1671
1680
|
await parser.parse();
|
|
1672
|
-
} catch (
|
|
1681
|
+
} catch (err) {
|
|
1673
1682
|
// If parsing fails during analysis pass, just skip it—we’ll still have
|
|
1674
1683
|
// partial frequency data from what we could parse
|
|
1675
1684
|
if (!options.continueOnParseError) {
|
|
1676
|
-
throw
|
|
1685
|
+
throw err;
|
|
1677
1686
|
}
|
|
1678
1687
|
}
|
|
1679
1688
|
}
|
|
@@ -1762,7 +1771,21 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
1762
1771
|
}
|
|
1763
1772
|
if (classChain) {
|
|
1764
1773
|
const sorter = classChain.createSorter();
|
|
1774
|
+
// Memoize `sortClassName` results—class lists often repeat in templates
|
|
1775
|
+
const classNameCache = new LRU(200);
|
|
1776
|
+
|
|
1765
1777
|
options.sortClassName = function (value) {
|
|
1778
|
+
// Fast path: Single class (no spaces) needs no sorting
|
|
1779
|
+
if (value.indexOf(' ') === -1) {
|
|
1780
|
+
return value;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// Check cache first
|
|
1784
|
+
const cached = classNameCache.get(value);
|
|
1785
|
+
if (cached !== undefined) {
|
|
1786
|
+
return cached;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1766
1789
|
// Expand UID tokens back to original content before sorting
|
|
1767
1790
|
// Fast path: Skip if no HTML comments (UID markers) present
|
|
1768
1791
|
let expandedValue = value;
|
|
@@ -1777,7 +1800,11 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
1777
1800
|
return cls !== '';
|
|
1778
1801
|
});
|
|
1779
1802
|
const sorted = sorter.sort(classes);
|
|
1780
|
-
|
|
1803
|
+
const result = sorted.join(' ');
|
|
1804
|
+
|
|
1805
|
+
// Cache the result
|
|
1806
|
+
classNameCache.set(value, result);
|
|
1807
|
+
return result;
|
|
1781
1808
|
};
|
|
1782
1809
|
}
|
|
1783
1810
|
}
|
|
@@ -1818,7 +1845,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1818
1845
|
const customElementsInput = options.inlineCustomElements ?? [];
|
|
1819
1846
|
const customElementsArr = Array.isArray(customElementsInput) ? customElementsInput : Array.from(customElementsInput);
|
|
1820
1847
|
const normalizedCustomElements = customElementsArr.map(name => options.name(name));
|
|
1821
|
-
// Fast path:
|
|
1848
|
+
// Fast path: Reuse base Sets if no custom elements
|
|
1822
1849
|
const inlineTextSet = normalizedCustomElements.length
|
|
1823
1850
|
? new Set([...inlineElementsToKeepWhitespaceWithin, ...normalizedCustomElements])
|
|
1824
1851
|
: inlineElementsToKeepWhitespaceWithin;
|
package/src/htmlparser.js
CHANGED
|
@@ -85,7 +85,7 @@ const preCompiledStackedTags = {
|
|
|
85
85
|
// Cache for compiled attribute regexes per handler configuration
|
|
86
86
|
const attrRegexCache = new WeakMap();
|
|
87
87
|
|
|
88
|
-
function
|
|
88
|
+
function buildAttrRegex(handler) {
|
|
89
89
|
let pattern = singleAttrIdentifier.source +
|
|
90
90
|
'(?:\\s*(' + joinSingleAttrAssigns(handler) + ')' +
|
|
91
91
|
'[ \\t\\n\\f\\r]*(?:' + singleAttrValues.join('|') + '))?';
|
|
@@ -104,6 +104,14 @@ function attrForHandler(handler) {
|
|
|
104
104
|
return new RegExp('^\\s*' + pattern);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
function getAttrRegexForHandler(handler) {
|
|
108
|
+
let cached = attrRegexCache.get(handler);
|
|
109
|
+
if (cached) return cached;
|
|
110
|
+
const compiled = buildAttrRegex(handler);
|
|
111
|
+
attrRegexCache.set(handler, compiled);
|
|
112
|
+
return compiled;
|
|
113
|
+
}
|
|
114
|
+
|
|
107
115
|
function joinSingleAttrAssigns(handler) {
|
|
108
116
|
return singleAttrAssigns.concat(
|
|
109
117
|
handler.customAttrAssign || []
|
|
@@ -127,12 +135,8 @@ export class HTMLParser {
|
|
|
127
135
|
const fullLength = fullHtml.length;
|
|
128
136
|
|
|
129
137
|
const stack = []; let lastTag;
|
|
130
|
-
// Use cached attribute regex
|
|
131
|
-
|
|
132
|
-
if (!attribute) {
|
|
133
|
-
attribute = attrForHandler(handler);
|
|
134
|
-
attrRegexCache.set(handler, attribute);
|
|
135
|
-
}
|
|
138
|
+
// Use cached attribute regex for this handler configuration
|
|
139
|
+
const attribute = getAttrRegexForHandler(handler);
|
|
136
140
|
let prevTag = undefined, nextTag = undefined;
|
|
137
141
|
|
|
138
142
|
// Index-based parsing
|