html-minifier-next 4.8.3 → 4.9.1
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 +43 -26
- package/cli.js +75 -8
- package/dist/htmlminifier.cjs +75 -0
- package/dist/htmlminifier.esm.bundle.js +75 -0
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/htmlminifier.js +75 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
HTML Minifier Next (HMN) is a **super-configurable, well-tested, JavaScript-based HTML minifier**.
|
|
6
6
|
|
|
7
|
-
The project was based on [HTML Minifier Terser](https://github.com/terser/html-minifier-terser), which in turn had been based on [Juriy Zaytsev’s HTML Minifier](https://github.com/kangax/html-minifier). HMN offers additional features, but is backwards-compatible with both. The project was set up because as of 2025, both HTML Minifier Terser and HTML Minifier had been unmaintained for a few years. As the project seems maintainable [to me, [Jens](https://meiert.com/)]—even more so with community support—, it’s being [updated, extended, and documented](https://github.com/j9t/html-minifier-next/blob/main/CHANGELOG.md) further in this place.
|
|
7
|
+
The project was based on [HTML Minifier Terser](https://github.com/terser/html-minifier-terser), which in turn had been based on [Juriy “kangax” Zaytsev’s HTML Minifier](https://github.com/kangax/html-minifier). HMN offers additional features, but is backwards-compatible with both. The project was set up because as of 2025, both HTML Minifier Terser and HTML Minifier had been unmaintained for a few years. As the project seems maintainable [to me, [Jens](https://meiert.com/), an HTML optimizer]—even more so with community support—, it’s being [updated, extended, and documented](https://github.com/j9t/html-minifier-next/blob/main/CHANGELOG.md) further in this place.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
@@ -29,11 +29,12 @@ Use `html-minifier-next --help` to check all available options:
|
|
|
29
29
|
| Option | Description | Example |
|
|
30
30
|
| --- | --- | --- |
|
|
31
31
|
| `--input-dir <dir>` | Specify an input directory (best restricted with `--file-ext`) | `--input-dir=src` |
|
|
32
|
+
| `--ignore-dir <patterns>` | Exclude directories—relative to input directory—from processing (comma-separated, overrides config file setting) | `--ignore-dir=libs`, `--ignore-dir=libs,vendor,node_modules` |
|
|
32
33
|
| `--output-dir <dir>` | Specify an output directory | `--output-dir=dist` |
|
|
33
|
-
| `--file-ext <extensions>` | Specify file extension(s) to process (overrides config file setting) | `--file-ext=html`, `--file-ext=html,htm,php`, `--file-ext="html, htm, php"` |
|
|
34
|
+
| `--file-ext <extensions>` | Specify file extension(s) to process (comma-separated, overrides config file setting) | `--file-ext=html`, `--file-ext=html,htm,php`, `--file-ext="html, htm, php"` |
|
|
34
35
|
| `-o <file>`, `--output <file>` | Specify output file (reads from file arguments or STDIN) | File to file: `html-minifier-next input.html -o output.html`<br>Pipe to file: `cat input.html \| html-minifier-next -o output.html`<br>File to STDOUT: `html-minifier-next input.html` |
|
|
35
36
|
| `-c <file>`, `--config-file <file>` | Use a configuration file | `--config-file=html-minifier.json` |
|
|
36
|
-
| `--preset <name>` | Use a preset configuration (conservative
|
|
37
|
+
| `--preset <name>` | Use a preset configuration (conservative or comprehensive) | `--preset=conservative` |
|
|
37
38
|
| `-v`, `--verbose` | Show detailed processing information (active options, file statistics) | `html-minifier-next --input-dir=src --output-dir=dist --verbose --collapse-whitespace` |
|
|
38
39
|
| `-d`, `--dry` | Dry run: Process and report statistics without writing output | `html-minifier-next input.html --dry --collapse-whitespace` |
|
|
39
40
|
| `-V`, `--version` | Output the version number | `html-minifier-next --version` |
|
|
@@ -48,7 +49,8 @@ You can also use a configuration file to specify options. The file can be either
|
|
|
48
49
|
{
|
|
49
50
|
"collapseWhitespace": true,
|
|
50
51
|
"removeComments": true,
|
|
51
|
-
"fileExt": "html,htm"
|
|
52
|
+
"fileExt": "html,htm",
|
|
53
|
+
"ignoreDir": "libs,vendor"
|
|
52
54
|
}
|
|
53
55
|
```
|
|
54
56
|
|
|
@@ -58,7 +60,8 @@ You can also use a configuration file to specify options. The file can be either
|
|
|
58
60
|
module.exports = {
|
|
59
61
|
collapseWhitespace: true,
|
|
60
62
|
removeComments: true,
|
|
61
|
-
fileExt: "html,htm"
|
|
63
|
+
fileExt: "html,htm",
|
|
64
|
+
ignoreDir: ["libs", "vendor"]
|
|
62
65
|
};
|
|
63
66
|
```
|
|
64
67
|
|
|
@@ -230,29 +233,30 @@ How does HTML Minifier Next compare to other minifiers? (All with the most aggre
|
|
|
230
233
|
| 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/) |
|
|
231
234
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
232
235
|
| [A List Apart](https://alistapart.com/) | 59 | **49** | 50 | 51 | 52 | 51 | 54 | 52 |
|
|
233
|
-
| [Apple](https://www.apple.com/) |
|
|
234
|
-
| [BBC](https://www.bbc.co.uk/) |
|
|
235
|
-
| [CSS-Tricks](https://css-tricks.com/) | 161 | 121 | **119** | 126 |
|
|
236
|
+
| [Apple](https://www.apple.com/) | 260 | **204** | **204** | 232 | 235 | 236 | 238 | 239 |
|
|
237
|
+
| [BBC](https://www.bbc.co.uk/) | 803 | **683** | 693 | 746 | 757 | 760 | 796 | n/a |
|
|
238
|
+
| [CSS-Tricks](https://css-tricks.com/) | 161 | 121 | **119** | 126 | 142 | 142 | 147 | 143 |
|
|
236
239
|
| [ECMAScript](https://tc39.es/ecma262/) | 7238 | **6341** | **6341** | 6561 | 6444 | 6567 | 6614 | n/a |
|
|
237
|
-
| [EFF](https://www.eff.org/) |
|
|
238
|
-
| [FAZ](https://www.faz.net/aktuell/) |
|
|
239
|
-
| [Frontend Dogma](https://frontenddogma.com/) | 222 | **
|
|
240
|
-
| [Google](https://www.google.com/) |
|
|
240
|
+
| [EFF](https://www.eff.org/) | 56 | **47** | 48 | 50 | 49 | 49 | 51 | 51 |
|
|
241
|
+
| [FAZ](https://www.faz.net/aktuell/) | 1602 | 1492 | 1497 | **1435** | 1525 | 1537 | 1548 | n/a |
|
|
242
|
+
| [Frontend Dogma](https://frontenddogma.com/) | 222 | **212** | 213 | 234 | 219 | 221 | 240 | 221 |
|
|
243
|
+
| [Google](https://www.google.com/) | 18 | **17** | **17** | **17** | **17** | **17** | 18 | 18 |
|
|
244
|
+
| [Ground News](https://ground.news/) | 1576 | **1353** | 1356 | 1450 | 1473 | 1478 | 1563 | n/a |
|
|
241
245
|
| [HTML Living Standard](https://html.spec.whatwg.org/multipage/) | 149 | **147** | **147** | 153 | **147** | 149 | 155 | 149 |
|
|
242
246
|
| [Igalia](https://www.igalia.com/) | 49 | **33** | **33** | 35 | 35 | 35 | 36 | 36 |
|
|
243
|
-
| [Leanpub](https://leanpub.com/) |
|
|
244
|
-
| [Mastodon](https://mastodon.social/explore) |
|
|
245
|
-
| [MDN](https://developer.mozilla.org/en-US/) | 107 | **
|
|
246
|
-
| [Middle East Eye](https://www.middleeasteye.net/) |
|
|
247
|
-
| [Nielsen Norman Group](https://www.nngroup.com/) | 84 |
|
|
248
|
-
| [SitePoint](https://www.sitepoint.com/) |
|
|
249
|
-
| [TetraLogical](https://tetralogical.com/) | 44 | 38 | 38 | **35** |
|
|
250
|
-
| [TPGi](https://www.tpgi.com/) |
|
|
251
|
-
| [United Nations](https://www.un.org/en/) |
|
|
252
|
-
| [W3C](https://www.w3.org/) | 50 | **
|
|
253
|
-
| **Average processing time** | |
|
|
254
|
-
|
|
255
|
-
(Last updated: Dec
|
|
247
|
+
| [Leanpub](https://leanpub.com/) | 2036 | **1755** | **1755** | 1762 | 1761 | 1759 | 2031 | n/a |
|
|
248
|
+
| [Mastodon](https://mastodon.social/explore) | 36 | **27** | **27** | 31 | 34 | 34 | 35 | 35 |
|
|
249
|
+
| [MDN](https://developer.mozilla.org/en-US/) | 107 | **61** | **61** | 63 | 63 | 64 | 66 | 67 |
|
|
250
|
+
| [Middle East Eye](https://www.middleeasteye.net/) | 223 | **195** | 196 | 203 | 201 | 200 | 202 | 203 |
|
|
251
|
+
| [Nielsen Norman Group](https://www.nngroup.com/) | 84 | 72 | 72 | **53** | 72 | 73 | 74 | 73 |
|
|
252
|
+
| [SitePoint](https://www.sitepoint.com/) | 487 | **346** | **346** | 424 | 461 | 466 | 484 | n/a |
|
|
253
|
+
| [TetraLogical](https://tetralogical.com/) | 44 | 38 | 38 | **35** | 38 | 38 | 39 | 39 |
|
|
254
|
+
| [TPGi](https://www.tpgi.com/) | 175 | **160** | 162 | **160** | 165 | 166 | 172 | 172 |
|
|
255
|
+
| [United Nations](https://www.un.org/en/) | 150 | **112** | 113 | 120 | 124 | 124 | 129 | 122 |
|
|
256
|
+
| [W3C](https://www.w3.org/) | 50 | **35** | 36 | 38 | 38 | 38 | 40 | 38 |
|
|
257
|
+
| **Average processing time** | | 338 ms (22/22) | 385 ms (22/22) | 191 ms (22/22) | 70 ms (22/22) | **18 ms (22/22)** | 365 ms (22/22) | 1403 ms (16/22) |
|
|
258
|
+
|
|
259
|
+
(Last updated: Dec 12, 2025)
|
|
256
260
|
<!-- End auto-generated -->
|
|
257
261
|
|
|
258
262
|
## Examples
|
|
@@ -283,6 +287,19 @@ html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist
|
|
|
283
287
|
# Consider restricting with `--file-ext` to avoid touching binaries (e.g., images, archives)
|
|
284
288
|
```
|
|
285
289
|
|
|
290
|
+
**Exclude directories from processing:**
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
# Ignore a single directory
|
|
294
|
+
html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --ignore-dir=libs
|
|
295
|
+
|
|
296
|
+
# Ignore multiple directories
|
|
297
|
+
html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --ignore-dir=libs,vendor,node_modules
|
|
298
|
+
|
|
299
|
+
# Ignore by relative path (only ignores src/static/libs, not other “libs” directories)
|
|
300
|
+
html-minifier-next --collapse-whitespace --input-dir=src --output-dir=dist --ignore-dir=static/libs
|
|
301
|
+
```
|
|
302
|
+
|
|
286
303
|
**Dry run mode (preview outcome without writing files):**
|
|
287
304
|
|
|
288
305
|
```bash
|
|
@@ -414,4 +431,4 @@ npm run benchmarks
|
|
|
414
431
|
|
|
415
432
|
## Acknowledgements
|
|
416
433
|
|
|
417
|
-
With many thanks to all the previous authors of HTML Minifier, especially [Juriy Zaytsev](https://github.com/kangax), and to everyone who helped make this new edition better, particularly [Daniel Ruf](https://github.com/DanielRuf) and [Jonas Geiler](https://github.com/jonasgeiler).
|
|
434
|
+
With many thanks to all the previous authors of HTML Minifier, especially [Juriy “kangax” Zaytsev](https://github.com/kangax), and to everyone who helped make this new edition better, particularly [Daniel Ruf](https://github.com/DanielRuf) and [Jonas Geiler](https://github.com/jonasgeiler).
|
package/cli.js
CHANGED
|
@@ -247,6 +247,14 @@ function normalizeConfig(config) {
|
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
// Handle `ignoreDir` in config file
|
|
251
|
+
if ('ignoreDir' in normalized) {
|
|
252
|
+
// Support both string (`libs,vendor`) and array (`["libs", "vendor"]`) formats
|
|
253
|
+
if (Array.isArray(normalized.ignoreDir)) {
|
|
254
|
+
normalized.ignoreDir = normalized.ignoreDir.join(',');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
250
258
|
return normalized;
|
|
251
259
|
}
|
|
252
260
|
|
|
@@ -254,8 +262,9 @@ let config = {};
|
|
|
254
262
|
program.option('-c --config-file <file>', 'Use config file');
|
|
255
263
|
program.option('--preset <name>', `Use a preset configuration (${getPresetNames().join(', ')})`);
|
|
256
264
|
program.option('--input-dir <dir>', 'Specify an input directory');
|
|
265
|
+
program.option('--ignore-dir <patterns>', 'Exclude directories—relative to input directory—from processing (comma-separated), e.g., “libs” or “libs,vendor,node_modules”');
|
|
257
266
|
program.option('--output-dir <dir>', 'Specify an output directory');
|
|
258
|
-
program.option('--file-ext <extensions>', 'Specify file extension(s) to process (comma-separated), e.g.,
|
|
267
|
+
program.option('--file-ext <extensions>', 'Specify file extension(s) to process (comma-separated), e.g., “html” or “html,htm,php”');
|
|
259
268
|
|
|
260
269
|
(async () => {
|
|
261
270
|
let content;
|
|
@@ -375,7 +384,42 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
375
384
|
return fileExtensions.includes(fileExt);
|
|
376
385
|
}
|
|
377
386
|
|
|
378
|
-
|
|
387
|
+
/**
|
|
388
|
+
* Parse comma-separated ignore patterns into an array
|
|
389
|
+
* @param {string} patterns - Comma-separated directory patterns (e.g., "libs,vendor")
|
|
390
|
+
* @returns {string[]} Array of trimmed pattern strings with normalized separators
|
|
391
|
+
*/
|
|
392
|
+
function parseIgnorePatterns(patterns) {
|
|
393
|
+
if (!patterns) return [];
|
|
394
|
+
return patterns
|
|
395
|
+
.split(',')
|
|
396
|
+
.map(p => p.trim().replace(/\\/g, '/').replace(/\/+$/, ''))
|
|
397
|
+
.filter(p => p.length > 0);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Check if a directory should be ignored based on ignore patterns
|
|
402
|
+
* Supports matching by directory name or relative path
|
|
403
|
+
* @param {string} dirPath - Absolute path to the directory
|
|
404
|
+
* @param {string[]} ignorePatterns - Array of patterns to match against (with forward slashes)
|
|
405
|
+
* @param {string} baseDir - Base directory for relative path calculation
|
|
406
|
+
* @returns {boolean} True if directory should be ignored
|
|
407
|
+
*/
|
|
408
|
+
function shouldIgnoreDirectory(dirPath, ignorePatterns, baseDir) {
|
|
409
|
+
if (!ignorePatterns || ignorePatterns.length === 0) return false;
|
|
410
|
+
|
|
411
|
+
// Normalize to forward slashes for cross-platform comparison
|
|
412
|
+
const relativePath = path.relative(baseDir, dirPath).replace(/\\/g, '/');
|
|
413
|
+
const dirName = path.basename(dirPath);
|
|
414
|
+
|
|
415
|
+
return ignorePatterns.some(pattern => {
|
|
416
|
+
// Support both exact directory names and relative paths
|
|
417
|
+
return dirName === pattern || relativePath === pattern ||
|
|
418
|
+
relativePath.startsWith(pattern + '/');
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function countFiles(dir, extensions, skipRootAbs, ignorePatterns, baseDir) {
|
|
379
423
|
let count = 0;
|
|
380
424
|
|
|
381
425
|
const files = await fs.promises.readdir(dir).catch(() => []);
|
|
@@ -397,7 +441,11 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
397
441
|
}
|
|
398
442
|
|
|
399
443
|
if (lst.isDirectory()) {
|
|
400
|
-
|
|
444
|
+
// Skip ignored directories
|
|
445
|
+
if (shouldIgnoreDirectory(filePath, ignorePatterns, baseDir)) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
count += await countFiles(filePath, extensions, skipRootAbs, ignorePatterns, baseDir);
|
|
401
449
|
} else if (shouldProcessFile(file, extensions)) {
|
|
402
450
|
count++;
|
|
403
451
|
}
|
|
@@ -423,12 +471,17 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
423
471
|
process.stderr.write('\r\x1b[K'); // Clear the line
|
|
424
472
|
}
|
|
425
473
|
|
|
426
|
-
async function processDirectory(inputDir, outputDir, extensions, isDryRun = false, isVerbose = false, skipRootAbs, progress = null) {
|
|
474
|
+
async function processDirectory(inputDir, outputDir, extensions, isDryRun = false, isVerbose = false, skipRootAbs, progress = null, ignorePatterns = [], baseDir = null) {
|
|
427
475
|
// If first call provided a string, normalize once; otherwise assume pre-parsed array
|
|
428
476
|
if (typeof extensions === 'string') {
|
|
429
477
|
extensions = parseFileExtensions(extensions);
|
|
430
478
|
}
|
|
431
479
|
|
|
480
|
+
// Set `baseDir` on first call
|
|
481
|
+
if (baseDir === null) {
|
|
482
|
+
baseDir = inputDir;
|
|
483
|
+
}
|
|
484
|
+
|
|
432
485
|
const files = await fs.promises.readdir(inputDir).catch(err => {
|
|
433
486
|
fatal('Cannot read directory ' + inputDir + '\n' + err.message);
|
|
434
487
|
});
|
|
@@ -456,7 +509,11 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
456
509
|
}
|
|
457
510
|
|
|
458
511
|
if (lst.isDirectory()) {
|
|
459
|
-
|
|
512
|
+
// Skip ignored directories
|
|
513
|
+
if (shouldIgnoreDirectory(inputFile, ignorePatterns, baseDir)) {
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
const dirStats = await processDirectory(inputFile, outputFile, extensions, isDryRun, isVerbose, skipRootAbs, progress, ignorePatterns, baseDir);
|
|
460
517
|
if (dirStats) {
|
|
461
518
|
allStats.push(...dirStats);
|
|
462
519
|
}
|
|
@@ -535,12 +592,16 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
535
592
|
process.stdout.write(minified);
|
|
536
593
|
};
|
|
537
594
|
|
|
538
|
-
const { inputDir, outputDir, fileExt } = programOptions;
|
|
595
|
+
const { inputDir, outputDir, fileExt, ignoreDir } = programOptions;
|
|
539
596
|
|
|
540
597
|
// Resolve file extensions: CLI argument takes priority over config file, even if empty string
|
|
541
598
|
const hasCliFileExt = program.getOptionValueSource('fileExt') === 'cli';
|
|
542
599
|
const resolvedFileExt = hasCliFileExt ? fileExt : config.fileExt;
|
|
543
600
|
|
|
601
|
+
// Resolve ignore patterns: CLI argument takes priority over config file
|
|
602
|
+
const hasCliIgnoreDir = program.getOptionValueSource('ignoreDir') === 'cli';
|
|
603
|
+
const resolvedIgnoreDir = hasCliIgnoreDir ? ignoreDir : config.ignoreDir;
|
|
604
|
+
|
|
544
605
|
if (inputDir || outputDir) {
|
|
545
606
|
if (!inputDir) {
|
|
546
607
|
fatal('The option `output-dir` needs to be used with the option `input-dir`—if you are working with a single file, use `-o`');
|
|
@@ -581,6 +642,12 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
581
642
|
const showProgress = process.stderr.isTTY && !isVerbose;
|
|
582
643
|
let progress = null;
|
|
583
644
|
|
|
645
|
+
// Parse ignore patterns
|
|
646
|
+
const ignorePatterns = parseIgnorePatterns(resolvedIgnoreDir);
|
|
647
|
+
|
|
648
|
+
// Resolve base directory for consistent path comparisons
|
|
649
|
+
const inputDirResolved = await fs.promises.realpath(inputDir).catch(() => inputDir);
|
|
650
|
+
|
|
584
651
|
if (showProgress) {
|
|
585
652
|
// Start with indeterminate progress, count in background
|
|
586
653
|
progress = {current: 0, total: null};
|
|
@@ -591,7 +658,7 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
591
658
|
// then see the updated value once `countFiles` resolves,
|
|
592
659
|
// transitioning the indicator from indeterminate to determinate progress without race conditions.
|
|
593
660
|
const extensions = typeof resolvedFileExt === 'string' ? parseFileExtensions(resolvedFileExt) : resolvedFileExt;
|
|
594
|
-
countFiles(inputDir, extensions, skipRootAbs).then(total => {
|
|
661
|
+
countFiles(inputDir, extensions, skipRootAbs, ignorePatterns, inputDirResolved).then(total => {
|
|
595
662
|
if (progress) {
|
|
596
663
|
progress.total = total;
|
|
597
664
|
}
|
|
@@ -600,7 +667,7 @@ program.option('--file-ext <extensions>', 'Specify file extension(s) to process
|
|
|
600
667
|
});
|
|
601
668
|
}
|
|
602
669
|
|
|
603
|
-
const stats = await processDirectory(inputDir, outputDir, resolvedFileExt, programOptions.dry, isVerbose, skipRootAbs, progress);
|
|
670
|
+
const stats = await processDirectory(inputDir, outputDir, resolvedFileExt, programOptions.dry, isVerbose, skipRootAbs, progress, ignorePatterns, inputDirResolved);
|
|
604
671
|
|
|
605
672
|
// Show completion message and clear progress indicator
|
|
606
673
|
if (progress) {
|
package/dist/htmlminifier.cjs
CHANGED
|
@@ -2363,6 +2363,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2363
2363
|
const ignoredMarkupChunks = [];
|
|
2364
2364
|
const ignoredCustomMarkupChunks = [];
|
|
2365
2365
|
let uidIgnore;
|
|
2366
|
+
let uidIgnorePlaceholderPattern;
|
|
2366
2367
|
let uidAttr;
|
|
2367
2368
|
let uidPattern;
|
|
2368
2369
|
// Create inline tags/text sets with custom elements
|
|
@@ -2396,6 +2397,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2396
2397
|
if (!uidIgnore) {
|
|
2397
2398
|
uidIgnore = uniqueId(value);
|
|
2398
2399
|
const pattern = new RegExp('^' + uidIgnore + '([0-9]+)$');
|
|
2400
|
+
uidIgnorePlaceholderPattern = new RegExp('^<!--' + uidIgnore + '(\\d+)-->$');
|
|
2399
2401
|
if (options.ignoreCustomComments) {
|
|
2400
2402
|
options.ignoreCustomComments = options.ignoreCustomComments.slice();
|
|
2401
2403
|
} else {
|
|
@@ -2820,6 +2822,79 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2820
2822
|
optionalStartTag = '';
|
|
2821
2823
|
optionalEndTag = '';
|
|
2822
2824
|
}
|
|
2825
|
+
|
|
2826
|
+
// Optimize whitespace collapsing between consecutive `htmlmin:ignore` placeholder comments
|
|
2827
|
+
if (options.collapseWhitespace && text && uidIgnorePlaceholderPattern) {
|
|
2828
|
+
if (uidIgnorePlaceholderPattern.test(text)) {
|
|
2829
|
+
// Check if previous buffer items are: [ignore-placeholder, whitespace-only text]
|
|
2830
|
+
if (buffer.length >= 2) {
|
|
2831
|
+
const prevText = buffer[buffer.length - 1];
|
|
2832
|
+
const prevComment = buffer[buffer.length - 2];
|
|
2833
|
+
|
|
2834
|
+
// Check if previous item is whitespace-only and item before that is ignore-placeholder
|
|
2835
|
+
if (prevText && /^\s+$/.test(prevText) &&
|
|
2836
|
+
prevComment && uidIgnorePlaceholderPattern.test(prevComment)) {
|
|
2837
|
+
// Extract the index from both placeholders to check their content
|
|
2838
|
+
const currentMatch = text.match(uidIgnorePlaceholderPattern);
|
|
2839
|
+
const prevMatch = prevComment.match(uidIgnorePlaceholderPattern);
|
|
2840
|
+
|
|
2841
|
+
if (currentMatch && prevMatch) {
|
|
2842
|
+
const currentIndex = +currentMatch[1];
|
|
2843
|
+
const prevIndex = +prevMatch[1];
|
|
2844
|
+
|
|
2845
|
+
// Defensive bounds check to ensure indices are valid
|
|
2846
|
+
if (currentIndex < ignoredMarkupChunks.length && prevIndex < ignoredMarkupChunks.length) {
|
|
2847
|
+
const currentContent = ignoredMarkupChunks[currentIndex];
|
|
2848
|
+
const prevContent = ignoredMarkupChunks[prevIndex];
|
|
2849
|
+
|
|
2850
|
+
// Only collapse whitespace if both blocks contain HTML (start with `<`)
|
|
2851
|
+
// Don’t collapse if either contains plain text, as that would change meaning
|
|
2852
|
+
// Note: This check will match HTML comments (`<!-- … -->`), but the tag-name
|
|
2853
|
+
// regex below requires starting with a letter, so comments are intentionally
|
|
2854
|
+
// excluded by the `currentTagMatch && prevTagMatch` guard
|
|
2855
|
+
if (currentContent && prevContent && /^\s*</.test(currentContent) && /^\s*</.test(prevContent)) {
|
|
2856
|
+
// Extract tag names from the HTML content (excludes comments, processing instructions, etc.)
|
|
2857
|
+
const currentTagMatch = currentContent.match(/^\s*<([a-zA-Z][\w:-]*)/);
|
|
2858
|
+
const prevTagMatch = prevContent.match(/^\s*<([a-zA-Z][\w:-]*)/);
|
|
2859
|
+
|
|
2860
|
+
// Only collapse if both matched valid element tags (not comments/text)
|
|
2861
|
+
// and both tags are block-level (inline elements need whitespace preserved)
|
|
2862
|
+
if (currentTagMatch && prevTagMatch) {
|
|
2863
|
+
const currentTag = options.name(currentTagMatch[1]);
|
|
2864
|
+
const prevTag = options.name(prevTagMatch[1]);
|
|
2865
|
+
|
|
2866
|
+
// Don’t collapse between inline elements
|
|
2867
|
+
if (!inlineElements.has(currentTag) && !inlineElements.has(prevTag)) {
|
|
2868
|
+
// Collapse whitespace respecting context rules
|
|
2869
|
+
let collapsedText = prevText;
|
|
2870
|
+
|
|
2871
|
+
// Apply `collapseWhitespace` with appropriate context
|
|
2872
|
+
if (!stackNoTrimWhitespace.length && !stackNoCollapseWhitespace.length) {
|
|
2873
|
+
// Not in pre or other no-collapse context
|
|
2874
|
+
if (options.preserveLineBreaks && /[\n\r]/.test(prevText)) {
|
|
2875
|
+
// Preserve line break as single newline
|
|
2876
|
+
collapsedText = '\n';
|
|
2877
|
+
} else if (options.conservativeCollapse) {
|
|
2878
|
+
// Conservative mode: keep single space
|
|
2879
|
+
collapsedText = ' ';
|
|
2880
|
+
} else {
|
|
2881
|
+
// Aggressive mode: remove all whitespace
|
|
2882
|
+
collapsedText = '';
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
// Replace the whitespace in buffer
|
|
2887
|
+
buffer[buffer.length - 1] = collapsedText;
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2823
2898
|
buffer.push(text);
|
|
2824
2899
|
},
|
|
2825
2900
|
doctype: function (doctype) {
|
|
@@ -7505,6 +7505,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
7505
7505
|
const ignoredMarkupChunks = [];
|
|
7506
7506
|
const ignoredCustomMarkupChunks = [];
|
|
7507
7507
|
let uidIgnore;
|
|
7508
|
+
let uidIgnorePlaceholderPattern;
|
|
7508
7509
|
let uidAttr;
|
|
7509
7510
|
let uidPattern;
|
|
7510
7511
|
// Create inline tags/text sets with custom elements
|
|
@@ -7538,6 +7539,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
7538
7539
|
if (!uidIgnore) {
|
|
7539
7540
|
uidIgnore = uniqueId(value);
|
|
7540
7541
|
const pattern = new RegExp('^' + uidIgnore + '([0-9]+)$');
|
|
7542
|
+
uidIgnorePlaceholderPattern = new RegExp('^<!--' + uidIgnore + '(\\d+)-->$');
|
|
7541
7543
|
if (options.ignoreCustomComments) {
|
|
7542
7544
|
options.ignoreCustomComments = options.ignoreCustomComments.slice();
|
|
7543
7545
|
} else {
|
|
@@ -7962,6 +7964,79 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
7962
7964
|
optionalStartTag = '';
|
|
7963
7965
|
optionalEndTag = '';
|
|
7964
7966
|
}
|
|
7967
|
+
|
|
7968
|
+
// Optimize whitespace collapsing between consecutive `htmlmin:ignore` placeholder comments
|
|
7969
|
+
if (options.collapseWhitespace && text && uidIgnorePlaceholderPattern) {
|
|
7970
|
+
if (uidIgnorePlaceholderPattern.test(text)) {
|
|
7971
|
+
// Check if previous buffer items are: [ignore-placeholder, whitespace-only text]
|
|
7972
|
+
if (buffer.length >= 2) {
|
|
7973
|
+
const prevText = buffer[buffer.length - 1];
|
|
7974
|
+
const prevComment = buffer[buffer.length - 2];
|
|
7975
|
+
|
|
7976
|
+
// Check if previous item is whitespace-only and item before that is ignore-placeholder
|
|
7977
|
+
if (prevText && /^\s+$/.test(prevText) &&
|
|
7978
|
+
prevComment && uidIgnorePlaceholderPattern.test(prevComment)) {
|
|
7979
|
+
// Extract the index from both placeholders to check their content
|
|
7980
|
+
const currentMatch = text.match(uidIgnorePlaceholderPattern);
|
|
7981
|
+
const prevMatch = prevComment.match(uidIgnorePlaceholderPattern);
|
|
7982
|
+
|
|
7983
|
+
if (currentMatch && prevMatch) {
|
|
7984
|
+
const currentIndex = +currentMatch[1];
|
|
7985
|
+
const prevIndex = +prevMatch[1];
|
|
7986
|
+
|
|
7987
|
+
// Defensive bounds check to ensure indices are valid
|
|
7988
|
+
if (currentIndex < ignoredMarkupChunks.length && prevIndex < ignoredMarkupChunks.length) {
|
|
7989
|
+
const currentContent = ignoredMarkupChunks[currentIndex];
|
|
7990
|
+
const prevContent = ignoredMarkupChunks[prevIndex];
|
|
7991
|
+
|
|
7992
|
+
// Only collapse whitespace if both blocks contain HTML (start with `<`)
|
|
7993
|
+
// Don’t collapse if either contains plain text, as that would change meaning
|
|
7994
|
+
// Note: This check will match HTML comments (`<!-- … -->`), but the tag-name
|
|
7995
|
+
// regex below requires starting with a letter, so comments are intentionally
|
|
7996
|
+
// excluded by the `currentTagMatch && prevTagMatch` guard
|
|
7997
|
+
if (currentContent && prevContent && /^\s*</.test(currentContent) && /^\s*</.test(prevContent)) {
|
|
7998
|
+
// Extract tag names from the HTML content (excludes comments, processing instructions, etc.)
|
|
7999
|
+
const currentTagMatch = currentContent.match(/^\s*<([a-zA-Z][\w:-]*)/);
|
|
8000
|
+
const prevTagMatch = prevContent.match(/^\s*<([a-zA-Z][\w:-]*)/);
|
|
8001
|
+
|
|
8002
|
+
// Only collapse if both matched valid element tags (not comments/text)
|
|
8003
|
+
// and both tags are block-level (inline elements need whitespace preserved)
|
|
8004
|
+
if (currentTagMatch && prevTagMatch) {
|
|
8005
|
+
const currentTag = options.name(currentTagMatch[1]);
|
|
8006
|
+
const prevTag = options.name(prevTagMatch[1]);
|
|
8007
|
+
|
|
8008
|
+
// Don’t collapse between inline elements
|
|
8009
|
+
if (!inlineElements.has(currentTag) && !inlineElements.has(prevTag)) {
|
|
8010
|
+
// Collapse whitespace respecting context rules
|
|
8011
|
+
let collapsedText = prevText;
|
|
8012
|
+
|
|
8013
|
+
// Apply `collapseWhitespace` with appropriate context
|
|
8014
|
+
if (!stackNoTrimWhitespace.length && !stackNoCollapseWhitespace.length) {
|
|
8015
|
+
// Not in pre or other no-collapse context
|
|
8016
|
+
if (options.preserveLineBreaks && /[\n\r]/.test(prevText)) {
|
|
8017
|
+
// Preserve line break as single newline
|
|
8018
|
+
collapsedText = '\n';
|
|
8019
|
+
} else if (options.conservativeCollapse) {
|
|
8020
|
+
// Conservative mode: keep single space
|
|
8021
|
+
collapsedText = ' ';
|
|
8022
|
+
} else {
|
|
8023
|
+
// Aggressive mode: remove all whitespace
|
|
8024
|
+
collapsedText = '';
|
|
8025
|
+
}
|
|
8026
|
+
}
|
|
8027
|
+
|
|
8028
|
+
// Replace the whitespace in buffer
|
|
8029
|
+
buffer[buffer.length - 1] = collapsedText;
|
|
8030
|
+
}
|
|
8031
|
+
}
|
|
8032
|
+
}
|
|
8033
|
+
}
|
|
8034
|
+
}
|
|
8035
|
+
}
|
|
8036
|
+
}
|
|
8037
|
+
}
|
|
8038
|
+
}
|
|
8039
|
+
|
|
7965
8040
|
buffer.push(text);
|
|
7966
8041
|
},
|
|
7967
8042
|
doctype: function (doctype) {
|
|
@@ -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":"AAgvEO,8BAJI,MAAM,YACN,eAAe,GACb,OAAO,CAAC,MAAM,CAAC,CAQ3B;;;;;;;;;;;;UAvtES,MAAM;YACN,MAAM;YACN,MAAM;mBACN,MAAM;iBACN,MAAM;kBACN,MAAM;;;;;;;;;;;;;4BAQN,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,qBAAqB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,KAAK,OAAO;;;;;;;wBAMjG,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,SAAS,EAAE,iBAAiB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,KAAK,OAAO;;;;;;;;oBAMhH,OAAO;;;;;;;;gCAOP,OAAO;;;;;;;;kCAOP,OAAO;;;;;;;;yBAOP,OAAO;;;;;;;;2BAOP,OAAO;;;;;;;;4BAOP,OAAO;;;;;;;2BAOP,OAAO;;;;;;;;uBAMP,MAAM,EAAE;;;;;;yBAOR,MAAM;;;;;;yBAKN,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE;;;;;;;4BAKlB,MAAM,EAAE;;;;;;;oCAMR,MAAM;;;;;;;qBAMN,OAAO;;;;;;;YAMP,OAAO;;;;;;;;2BAMP,MAAM,EAAE;;;;;;;;;4BAOR,MAAM,EAAE;;;;;;;+BAQR,OAAO;;;;;;;2BAMP,SAAS,CAAC,MAAM,CAAC;;;;;;uBAMjB,OAAO;;;;;;;;UAKP,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI;;;;;;;;qBAO1B,MAAM;;;;;;;oBAON,MAAM;;;;;;;;;;gBAMN,OAAO,GAAG,OAAO,CAAC,OAAO,cAAc,EAAE,gBAAgB,CAAC,OAAO,cAAc,EAAE,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;;;;;;;;;;eAS9J,OAAO,GAAG,OAAO,QAAQ,EAAE,aAAa,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;;;;;;;;;;iBASzG,OAAO,GAAG,MAAM,GAAG,OAAO,WAAW,EAAE,OAAO,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;;;;;;;;WAS7F,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM;;;;;;;+BAOxB,OAAO;;;;;;;;;;oBAMP,OAAO;;;;;;;;yBASP,OAAO;;;;;;;gCAOP,OAAO;;;;;;;;iCAMP,OAAO;;;;;;;;;;qBAOP,MAAM,EAAE;;;;;;;qBASR,IAAI,GAAG,GAAG;;;;;;;4BAMV,OAAO;;;;;;;;qBAMP,OAAO;;;;;;;;;4BAOP,OAAO,GAAG,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;;;;;;;;0BAQtD,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;gCAOP,MAAM,EAAE;;;;;;;;yBAyBR,OAAO;;;;;;;;gCAOP,OAAO;;;;;;;iCAOP,OAAO;;;;;;;oCAMP,OAAO;;;;;;;;;;0BAMP,OAAO;;;;;;;;;qBASP,OAAO,GAAG,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,IAAI,CAAC;;;;;;;;;oBAQzD,OAAO,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;;;;;;;;0BAQrC,OAAO;;;;;;;sBAOP,OAAO;;wBAnYkC,cAAc;0BAAd,cAAc;+BAAd,cAAc"}
|
package/package.json
CHANGED
package/src/htmlminifier.js
CHANGED
|
@@ -1657,6 +1657,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1657
1657
|
const ignoredMarkupChunks = [];
|
|
1658
1658
|
const ignoredCustomMarkupChunks = [];
|
|
1659
1659
|
let uidIgnore;
|
|
1660
|
+
let uidIgnorePlaceholderPattern;
|
|
1660
1661
|
let uidAttr;
|
|
1661
1662
|
let uidPattern;
|
|
1662
1663
|
// Create inline tags/text sets with custom elements
|
|
@@ -1690,6 +1691,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1690
1691
|
if (!uidIgnore) {
|
|
1691
1692
|
uidIgnore = uniqueId(value);
|
|
1692
1693
|
const pattern = new RegExp('^' + uidIgnore + '([0-9]+)$');
|
|
1694
|
+
uidIgnorePlaceholderPattern = new RegExp('^<!--' + uidIgnore + '(\\d+)-->$');
|
|
1693
1695
|
if (options.ignoreCustomComments) {
|
|
1694
1696
|
options.ignoreCustomComments = options.ignoreCustomComments.slice();
|
|
1695
1697
|
} else {
|
|
@@ -2114,6 +2116,79 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2114
2116
|
optionalStartTag = '';
|
|
2115
2117
|
optionalEndTag = '';
|
|
2116
2118
|
}
|
|
2119
|
+
|
|
2120
|
+
// Optimize whitespace collapsing between consecutive `htmlmin:ignore` placeholder comments
|
|
2121
|
+
if (options.collapseWhitespace && text && uidIgnorePlaceholderPattern) {
|
|
2122
|
+
if (uidIgnorePlaceholderPattern.test(text)) {
|
|
2123
|
+
// Check if previous buffer items are: [ignore-placeholder, whitespace-only text]
|
|
2124
|
+
if (buffer.length >= 2) {
|
|
2125
|
+
const prevText = buffer[buffer.length - 1];
|
|
2126
|
+
const prevComment = buffer[buffer.length - 2];
|
|
2127
|
+
|
|
2128
|
+
// Check if previous item is whitespace-only and item before that is ignore-placeholder
|
|
2129
|
+
if (prevText && /^\s+$/.test(prevText) &&
|
|
2130
|
+
prevComment && uidIgnorePlaceholderPattern.test(prevComment)) {
|
|
2131
|
+
// Extract the index from both placeholders to check their content
|
|
2132
|
+
const currentMatch = text.match(uidIgnorePlaceholderPattern);
|
|
2133
|
+
const prevMatch = prevComment.match(uidIgnorePlaceholderPattern);
|
|
2134
|
+
|
|
2135
|
+
if (currentMatch && prevMatch) {
|
|
2136
|
+
const currentIndex = +currentMatch[1];
|
|
2137
|
+
const prevIndex = +prevMatch[1];
|
|
2138
|
+
|
|
2139
|
+
// Defensive bounds check to ensure indices are valid
|
|
2140
|
+
if (currentIndex < ignoredMarkupChunks.length && prevIndex < ignoredMarkupChunks.length) {
|
|
2141
|
+
const currentContent = ignoredMarkupChunks[currentIndex];
|
|
2142
|
+
const prevContent = ignoredMarkupChunks[prevIndex];
|
|
2143
|
+
|
|
2144
|
+
// Only collapse whitespace if both blocks contain HTML (start with `<`)
|
|
2145
|
+
// Don’t collapse if either contains plain text, as that would change meaning
|
|
2146
|
+
// Note: This check will match HTML comments (`<!-- … -->`), but the tag-name
|
|
2147
|
+
// regex below requires starting with a letter, so comments are intentionally
|
|
2148
|
+
// excluded by the `currentTagMatch && prevTagMatch` guard
|
|
2149
|
+
if (currentContent && prevContent && /^\s*</.test(currentContent) && /^\s*</.test(prevContent)) {
|
|
2150
|
+
// Extract tag names from the HTML content (excludes comments, processing instructions, etc.)
|
|
2151
|
+
const currentTagMatch = currentContent.match(/^\s*<([a-zA-Z][\w:-]*)/);
|
|
2152
|
+
const prevTagMatch = prevContent.match(/^\s*<([a-zA-Z][\w:-]*)/);
|
|
2153
|
+
|
|
2154
|
+
// Only collapse if both matched valid element tags (not comments/text)
|
|
2155
|
+
// and both tags are block-level (inline elements need whitespace preserved)
|
|
2156
|
+
if (currentTagMatch && prevTagMatch) {
|
|
2157
|
+
const currentTag = options.name(currentTagMatch[1]);
|
|
2158
|
+
const prevTag = options.name(prevTagMatch[1]);
|
|
2159
|
+
|
|
2160
|
+
// Don’t collapse between inline elements
|
|
2161
|
+
if (!inlineElements.has(currentTag) && !inlineElements.has(prevTag)) {
|
|
2162
|
+
// Collapse whitespace respecting context rules
|
|
2163
|
+
let collapsedText = prevText;
|
|
2164
|
+
|
|
2165
|
+
// Apply `collapseWhitespace` with appropriate context
|
|
2166
|
+
if (!stackNoTrimWhitespace.length && !stackNoCollapseWhitespace.length) {
|
|
2167
|
+
// Not in pre or other no-collapse context
|
|
2168
|
+
if (options.preserveLineBreaks && /[\n\r]/.test(prevText)) {
|
|
2169
|
+
// Preserve line break as single newline
|
|
2170
|
+
collapsedText = '\n';
|
|
2171
|
+
} else if (options.conservativeCollapse) {
|
|
2172
|
+
// Conservative mode: keep single space
|
|
2173
|
+
collapsedText = ' ';
|
|
2174
|
+
} else {
|
|
2175
|
+
// Aggressive mode: remove all whitespace
|
|
2176
|
+
collapsedText = '';
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
// Replace the whitespace in buffer
|
|
2181
|
+
buffer[buffer.length - 1] = collapsedText;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2117
2192
|
buffer.push(text);
|
|
2118
2193
|
},
|
|
2119
2194
|
doctype: function (doctype) {
|