html-minifier-next 4.9.2 → 4.11.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 +24 -19
- package/cli.js +3 -2
- package/dist/htmlminifier.cjs +166 -75
- package/dist/htmlminifier.esm.bundle.js +166 -75
- package/dist/types/htmlminifier.d.ts +9 -0
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/htmlparser.d.ts.map +1 -1
- package/dist/types/presets.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 +14 -0
- package/src/htmlparser.js +75 -42
- package/src/presets.js +1 -0
- package/src/tokenchain.js +77 -34
|
@@ -5231,6 +5231,9 @@ const preCompiledStackedTags = {
|
|
|
5231
5231
|
'noscript': /([\s\S]*?)<\/noscript[^>]*>/i
|
|
5232
5232
|
};
|
|
5233
5233
|
|
|
5234
|
+
// Cache for compiled attribute regexes per handler configuration
|
|
5235
|
+
const attrRegexCache = new WeakMap();
|
|
5236
|
+
|
|
5234
5237
|
function attrForHandler(handler) {
|
|
5235
5238
|
let pattern = singleAttrIdentifier.source +
|
|
5236
5239
|
'(?:\\s*(' + joinSingleAttrAssigns(handler) + ')' +
|
|
@@ -5268,22 +5271,47 @@ class HTMLParser {
|
|
|
5268
5271
|
}
|
|
5269
5272
|
|
|
5270
5273
|
async parse() {
|
|
5271
|
-
let html = this.html;
|
|
5272
5274
|
const handler = this.handler;
|
|
5275
|
+
const fullHtml = this.html;
|
|
5276
|
+
const fullLength = fullHtml.length;
|
|
5273
5277
|
|
|
5274
5278
|
const stack = []; let lastTag;
|
|
5275
|
-
|
|
5276
|
-
let
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5279
|
+
// Use cached attribute regex if available
|
|
5280
|
+
let attribute = attrRegexCache.get(handler);
|
|
5281
|
+
if (!attribute) {
|
|
5282
|
+
attribute = attrForHandler(handler);
|
|
5283
|
+
attrRegexCache.set(handler, attribute);
|
|
5284
|
+
}
|
|
5285
|
+
let prevTag = undefined, nextTag = undefined;
|
|
5286
|
+
|
|
5287
|
+
// Index-based parsing
|
|
5288
|
+
let pos = 0;
|
|
5289
|
+
let lastPos;
|
|
5290
|
+
|
|
5291
|
+
// Helper to get remaining HTML from current position
|
|
5292
|
+
const remaining = () => fullHtml.slice(pos);
|
|
5293
|
+
|
|
5294
|
+
// Helper to advance position
|
|
5295
|
+
const advance = (n) => { pos += n; };
|
|
5296
|
+
|
|
5297
|
+
// Lazy line/column calculation—only compute on actual errors
|
|
5298
|
+
const getLineColumn = (position) => {
|
|
5299
|
+
let line = 1;
|
|
5300
|
+
let column = 1;
|
|
5301
|
+
for (let i = 0; i < position; i++) {
|
|
5302
|
+
if (fullHtml[i] === '\n') {
|
|
5303
|
+
line++;
|
|
5304
|
+
column = 1;
|
|
5305
|
+
} else {
|
|
5306
|
+
column++;
|
|
5307
|
+
}
|
|
5308
|
+
}
|
|
5309
|
+
return { line, column };
|
|
5283
5310
|
};
|
|
5284
5311
|
|
|
5285
|
-
while (
|
|
5286
|
-
|
|
5312
|
+
while (pos < fullLength) {
|
|
5313
|
+
lastPos = pos;
|
|
5314
|
+
const html = remaining();
|
|
5287
5315
|
// Make sure we’re not in a `script` or `style` element
|
|
5288
5316
|
if (!lastTag || !special.has(lastTag)) {
|
|
5289
5317
|
let textEnd = html.indexOf('<');
|
|
@@ -5296,7 +5324,7 @@ class HTMLParser {
|
|
|
5296
5324
|
if (handler.comment) {
|
|
5297
5325
|
await handler.comment(html.substring(4, commentEnd));
|
|
5298
5326
|
}
|
|
5299
|
-
|
|
5327
|
+
advance(commentEnd + 3);
|
|
5300
5328
|
prevTag = '';
|
|
5301
5329
|
continue;
|
|
5302
5330
|
}
|
|
@@ -5310,7 +5338,7 @@ class HTMLParser {
|
|
|
5310
5338
|
if (handler.comment) {
|
|
5311
5339
|
await handler.comment(html.substring(2, conditionalEnd + 1), true /* non-standard */);
|
|
5312
5340
|
}
|
|
5313
|
-
|
|
5341
|
+
advance(conditionalEnd + 2);
|
|
5314
5342
|
prevTag = '';
|
|
5315
5343
|
continue;
|
|
5316
5344
|
}
|
|
@@ -5322,7 +5350,7 @@ class HTMLParser {
|
|
|
5322
5350
|
if (handler.doctype) {
|
|
5323
5351
|
handler.doctype(doctypeMatch[0]);
|
|
5324
5352
|
}
|
|
5325
|
-
|
|
5353
|
+
advance(doctypeMatch[0].length);
|
|
5326
5354
|
prevTag = '';
|
|
5327
5355
|
continue;
|
|
5328
5356
|
}
|
|
@@ -5330,7 +5358,7 @@ class HTMLParser {
|
|
|
5330
5358
|
// End tag
|
|
5331
5359
|
const endTagMatch = html.match(endTag);
|
|
5332
5360
|
if (endTagMatch) {
|
|
5333
|
-
|
|
5361
|
+
advance(endTagMatch[0].length);
|
|
5334
5362
|
await parseEndTag(endTagMatch[0], endTagMatch[1]);
|
|
5335
5363
|
prevTag = '/' + endTagMatch[1].toLowerCase();
|
|
5336
5364
|
continue;
|
|
@@ -5339,7 +5367,7 @@ class HTMLParser {
|
|
|
5339
5367
|
// Start tag
|
|
5340
5368
|
const startTagMatch = parseStartTag(html);
|
|
5341
5369
|
if (startTagMatch) {
|
|
5342
|
-
|
|
5370
|
+
advance(startTagMatch.advance);
|
|
5343
5371
|
await handleStartTag(startTagMatch);
|
|
5344
5372
|
prevTag = startTagMatch.tagName.toLowerCase();
|
|
5345
5373
|
continue;
|
|
@@ -5354,18 +5382,19 @@ class HTMLParser {
|
|
|
5354
5382
|
let text;
|
|
5355
5383
|
if (textEnd >= 0) {
|
|
5356
5384
|
text = html.substring(0, textEnd);
|
|
5357
|
-
|
|
5385
|
+
advance(textEnd);
|
|
5358
5386
|
} else {
|
|
5359
5387
|
text = html;
|
|
5360
|
-
html
|
|
5388
|
+
advance(html.length);
|
|
5361
5389
|
}
|
|
5362
5390
|
|
|
5363
5391
|
// Next tag
|
|
5364
|
-
|
|
5392
|
+
const nextHtml = remaining();
|
|
5393
|
+
let nextTagMatch = parseStartTag(nextHtml);
|
|
5365
5394
|
if (nextTagMatch) {
|
|
5366
5395
|
nextTag = nextTagMatch.tagName;
|
|
5367
5396
|
} else {
|
|
5368
|
-
nextTagMatch =
|
|
5397
|
+
nextTagMatch = nextHtml.match(endTag);
|
|
5369
5398
|
if (nextTagMatch) {
|
|
5370
5399
|
nextTag = '/' + nextTagMatch[1];
|
|
5371
5400
|
} else {
|
|
@@ -5394,41 +5423,38 @@ class HTMLParser {
|
|
|
5394
5423
|
await handler.chars(text);
|
|
5395
5424
|
}
|
|
5396
5425
|
// Advance HTML past the matched special tag content and its closing tag
|
|
5397
|
-
|
|
5426
|
+
advance(m.index + m[0].length);
|
|
5398
5427
|
await parseEndTag('</' + stackedTag + '>', stackedTag);
|
|
5399
5428
|
} else {
|
|
5400
5429
|
// No closing tag found; to avoid infinite loop, break similarly to previous behavior
|
|
5401
5430
|
if (handler.continueOnParseError && handler.chars && html) {
|
|
5402
5431
|
await handler.chars(html[0], prevTag, '');
|
|
5403
|
-
|
|
5432
|
+
advance(1);
|
|
5404
5433
|
} else {
|
|
5405
5434
|
break;
|
|
5406
5435
|
}
|
|
5407
5436
|
}
|
|
5408
5437
|
}
|
|
5409
5438
|
|
|
5410
|
-
if (
|
|
5439
|
+
if (pos === lastPos) {
|
|
5411
5440
|
if (handler.continueOnParseError) {
|
|
5412
5441
|
// Skip the problematic character and continue
|
|
5413
5442
|
if (handler.chars) {
|
|
5414
|
-
await handler.chars(
|
|
5443
|
+
await handler.chars(fullHtml[pos], prevTag, '');
|
|
5415
5444
|
}
|
|
5416
|
-
|
|
5417
|
-
position++;
|
|
5445
|
+
advance(1);
|
|
5418
5446
|
prevTag = '';
|
|
5419
5447
|
continue;
|
|
5420
5448
|
}
|
|
5421
|
-
const loc = getLineColumn(
|
|
5422
|
-
// Include some context before the error position so the snippet contains
|
|
5423
|
-
// the offending markup plus preceding characters (e.g. "invalid<tag").
|
|
5449
|
+
const loc = getLineColumn(pos);
|
|
5450
|
+
// Include some context before the error position so the snippet contains the offending markup plus preceding characters (e.g., “invalid<tag”)
|
|
5424
5451
|
const CONTEXT_BEFORE = 50;
|
|
5425
|
-
const startPos = Math.max(0,
|
|
5426
|
-
const snippet =
|
|
5452
|
+
const startPos = Math.max(0, pos - CONTEXT_BEFORE);
|
|
5453
|
+
const snippet = fullHtml.slice(startPos, startPos + 200).replace(/\n/g, ' ');
|
|
5427
5454
|
throw new Error(
|
|
5428
|
-
`Parse error at line ${loc.line}, column ${loc.column}:\n${snippet}${
|
|
5455
|
+
`Parse error at line ${loc.line}, column ${loc.column}:\n${snippet}${fullHtml.length > startPos + 200 ? '…' : ''}`
|
|
5429
5456
|
);
|
|
5430
5457
|
}
|
|
5431
|
-
position = this.html.length - html.length;
|
|
5432
5458
|
}
|
|
5433
5459
|
|
|
5434
5460
|
if (!handler.partialMarkup) {
|
|
@@ -5441,9 +5467,11 @@ class HTMLParser {
|
|
|
5441
5467
|
if (start) {
|
|
5442
5468
|
const match = {
|
|
5443
5469
|
tagName: start[1],
|
|
5444
|
-
attrs: []
|
|
5470
|
+
attrs: [],
|
|
5471
|
+
advance: 0
|
|
5445
5472
|
};
|
|
5446
|
-
|
|
5473
|
+
let consumed = start[0].length;
|
|
5474
|
+
input = input.slice(consumed);
|
|
5447
5475
|
let end, attr;
|
|
5448
5476
|
|
|
5449
5477
|
// Safety limit: max length of input to check for attributes
|
|
@@ -5493,7 +5521,9 @@ class HTMLParser {
|
|
|
5493
5521
|
} else {
|
|
5494
5522
|
attr[baseIndex + 3] = value; // Single-quoted value
|
|
5495
5523
|
}
|
|
5496
|
-
|
|
5524
|
+
const attrLen = fullAttr.length;
|
|
5525
|
+
input = input.slice(attrLen);
|
|
5526
|
+
consumed += attrLen;
|
|
5497
5527
|
match.attrs.push(attr);
|
|
5498
5528
|
continue;
|
|
5499
5529
|
}
|
|
@@ -5510,7 +5540,9 @@ class HTMLParser {
|
|
|
5510
5540
|
break;
|
|
5511
5541
|
}
|
|
5512
5542
|
|
|
5513
|
-
|
|
5543
|
+
const attrLen = attr[0].length;
|
|
5544
|
+
input = input.slice(attrLen);
|
|
5545
|
+
consumed += attrLen;
|
|
5514
5546
|
match.attrs.push(attr);
|
|
5515
5547
|
}
|
|
5516
5548
|
|
|
@@ -5518,7 +5550,8 @@ class HTMLParser {
|
|
|
5518
5550
|
end = input.match(startTagClose);
|
|
5519
5551
|
if (end) {
|
|
5520
5552
|
match.unarySlash = end[1];
|
|
5521
|
-
|
|
5553
|
+
consumed += end[0].length;
|
|
5554
|
+
match.advance = consumed;
|
|
5522
5555
|
return match;
|
|
5523
5556
|
}
|
|
5524
5557
|
}
|
|
@@ -5528,7 +5561,7 @@ class HTMLParser {
|
|
|
5528
5561
|
let pos;
|
|
5529
5562
|
const needle = tagName.toLowerCase();
|
|
5530
5563
|
for (pos = stack.length - 1; pos >= 0; pos--) {
|
|
5531
|
-
const currentTag = stack[pos].
|
|
5564
|
+
const currentTag = stack[pos].lowerTag;
|
|
5532
5565
|
if (currentTag === needle) {
|
|
5533
5566
|
return pos;
|
|
5534
5567
|
}
|
|
@@ -5582,7 +5615,7 @@ class HTMLParser {
|
|
|
5582
5615
|
}
|
|
5583
5616
|
if (tagName === 'col' && findTag('colgroup') < 0) {
|
|
5584
5617
|
lastTag = 'colgroup';
|
|
5585
|
-
stack.push({ tag: lastTag, attrs: [] });
|
|
5618
|
+
stack.push({ tag: lastTag, lowerTag: 'colgroup', attrs: [] });
|
|
5586
5619
|
if (handler.start) {
|
|
5587
5620
|
await handler.start(lastTag, [], false, '');
|
|
5588
5621
|
}
|
|
@@ -5661,7 +5694,7 @@ class HTMLParser {
|
|
|
5661
5694
|
});
|
|
5662
5695
|
|
|
5663
5696
|
if (!unary) {
|
|
5664
|
-
stack.push({ tag: tagName, attrs });
|
|
5697
|
+
stack.push({ tag: tagName, lowerTag: tagName.toLowerCase(), attrs });
|
|
5665
5698
|
lastTag = tagName;
|
|
5666
5699
|
unarySlash = '';
|
|
5667
5700
|
}
|
|
@@ -5675,7 +5708,7 @@ class HTMLParser {
|
|
|
5675
5708
|
let pos;
|
|
5676
5709
|
const needle = tagName.toLowerCase();
|
|
5677
5710
|
for (pos = stack.length - 1; pos >= 0; pos--) {
|
|
5678
|
-
if (stack[pos].
|
|
5711
|
+
if (stack[pos].lowerTag === needle) {
|
|
5679
5712
|
break;
|
|
5680
5713
|
}
|
|
5681
5714
|
}
|
|
@@ -5727,21 +5760,40 @@ class HTMLParser {
|
|
|
5727
5760
|
class Sorter {
|
|
5728
5761
|
sort(tokens, fromIndex = 0) {
|
|
5729
5762
|
for (let i = 0, len = this.keys.length; i < len; i++) {
|
|
5730
|
-
const
|
|
5731
|
-
const token = key.slice(1);
|
|
5763
|
+
const token = this.keys[i];
|
|
5732
5764
|
|
|
5733
|
-
|
|
5765
|
+
// Build position map for this token to avoid repeated `indexOf`
|
|
5766
|
+
const positions = [];
|
|
5767
|
+
for (let j = fromIndex; j < tokens.length; j++) {
|
|
5768
|
+
if (tokens[j] === token) {
|
|
5769
|
+
positions.push(j);
|
|
5770
|
+
}
|
|
5771
|
+
}
|
|
5734
5772
|
|
|
5735
|
-
if (
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5773
|
+
if (positions.length > 0) {
|
|
5774
|
+
// Build new array with tokens in sorted order instead of splicing
|
|
5775
|
+
const result = [];
|
|
5776
|
+
|
|
5777
|
+
// Add all instances of the current token first
|
|
5778
|
+
for (let j = 0; j < positions.length; j++) {
|
|
5779
|
+
result.push(token);
|
|
5780
|
+
}
|
|
5781
|
+
|
|
5782
|
+
// Add other tokens, skipping positions where current token was
|
|
5783
|
+
const posSet = new Set(positions);
|
|
5784
|
+
for (let j = fromIndex; j < tokens.length; j++) {
|
|
5785
|
+
if (!posSet.has(j)) {
|
|
5786
|
+
result.push(tokens[j]);
|
|
5740
5787
|
}
|
|
5741
|
-
|
|
5742
|
-
} while ((index = tokens.indexOf(token, fromIndex)) !== -1);
|
|
5788
|
+
}
|
|
5743
5789
|
|
|
5744
|
-
|
|
5790
|
+
// Copy sorted portion back to tokens array
|
|
5791
|
+
for (let j = 0; j < result.length; j++) {
|
|
5792
|
+
tokens[fromIndex + j] = result[j];
|
|
5793
|
+
}
|
|
5794
|
+
|
|
5795
|
+
const newFromIndex = fromIndex + positions.length;
|
|
5796
|
+
return this.sorterMap.get(token).sort(tokens, newFromIndex);
|
|
5745
5797
|
}
|
|
5746
5798
|
}
|
|
5747
5799
|
return tokens;
|
|
@@ -5749,44 +5801,68 @@ class Sorter {
|
|
|
5749
5801
|
}
|
|
5750
5802
|
|
|
5751
5803
|
class TokenChain {
|
|
5804
|
+
constructor() {
|
|
5805
|
+
// Use Map instead of object properties for better performance
|
|
5806
|
+
this.map = new Map();
|
|
5807
|
+
}
|
|
5808
|
+
|
|
5752
5809
|
add(tokens) {
|
|
5753
5810
|
tokens.forEach((token) => {
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
this[key] = [];
|
|
5757
|
-
this[key].processed = 0;
|
|
5811
|
+
if (!this.map.has(token)) {
|
|
5812
|
+
this.map.set(token, { arrays: [], processed: 0 });
|
|
5758
5813
|
}
|
|
5759
|
-
this
|
|
5814
|
+
this.map.get(token).arrays.push(tokens);
|
|
5760
5815
|
});
|
|
5761
5816
|
}
|
|
5762
5817
|
|
|
5763
5818
|
createSorter() {
|
|
5764
5819
|
const sorter = new Sorter();
|
|
5820
|
+
sorter.sorterMap = new Map();
|
|
5821
|
+
|
|
5822
|
+
// Convert Map entries to array and sort
|
|
5823
|
+
const entries = Array.from(this.map.entries()).sort((a, b) => {
|
|
5824
|
+
const m = a[1].arrays.length;
|
|
5825
|
+
const n = b[1].arrays.length;
|
|
5826
|
+
// Sort by length descending (larger first)
|
|
5827
|
+
const lengthDiff = n - m;
|
|
5828
|
+
if (lengthDiff !== 0) return lengthDiff;
|
|
5829
|
+
// If lengths equal, sort by key ascending
|
|
5830
|
+
return a[0].localeCompare(b[0]);
|
|
5831
|
+
});
|
|
5765
5832
|
|
|
5766
|
-
sorter.keys =
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
}).filter((key) => {
|
|
5771
|
-
if (this[key].processed < this[key].length) {
|
|
5772
|
-
const token = key.slice(1);
|
|
5833
|
+
sorter.keys = [];
|
|
5834
|
+
|
|
5835
|
+
entries.forEach(([token, data]) => {
|
|
5836
|
+
if (data.processed < data.arrays.length) {
|
|
5773
5837
|
const chain = new TokenChain();
|
|
5774
5838
|
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5839
|
+
data.arrays.forEach((tokens) => {
|
|
5840
|
+
// Build new array without the current token instead of splicing
|
|
5841
|
+
const filtered = [];
|
|
5842
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
5843
|
+
if (tokens[i] !== token) {
|
|
5844
|
+
filtered.push(tokens[i]);
|
|
5845
|
+
}
|
|
5779
5846
|
}
|
|
5780
|
-
|
|
5781
|
-
|
|
5847
|
+
|
|
5848
|
+
// Mark remaining tokens as processed
|
|
5849
|
+
filtered.forEach((t) => {
|
|
5850
|
+
const tData = this.map.get(t);
|
|
5851
|
+
if (tData) {
|
|
5852
|
+
tData.processed++;
|
|
5853
|
+
}
|
|
5782
5854
|
});
|
|
5783
|
-
|
|
5855
|
+
|
|
5856
|
+
if (filtered.length > 0) {
|
|
5857
|
+
chain.add(filtered);
|
|
5858
|
+
}
|
|
5784
5859
|
});
|
|
5785
|
-
|
|
5786
|
-
|
|
5860
|
+
|
|
5861
|
+
sorter.keys.push(token);
|
|
5862
|
+
sorter.sorterMap.set(token, chain.createSorter());
|
|
5787
5863
|
}
|
|
5788
|
-
return false;
|
|
5789
5864
|
});
|
|
5865
|
+
|
|
5790
5866
|
return sorter;
|
|
5791
5867
|
}
|
|
5792
5868
|
}
|
|
@@ -5827,6 +5903,7 @@ const presets = {
|
|
|
5827
5903
|
useShortDoctype: true
|
|
5828
5904
|
},
|
|
5829
5905
|
comprehensive: {
|
|
5906
|
+
// @@ Add `collapseAttributeWhitespace: true` (also add to preset in demo)
|
|
5830
5907
|
caseSensitive: true,
|
|
5831
5908
|
collapseBooleanAttributes: true,
|
|
5832
5909
|
collapseInlineTagWhitespace: true,
|
|
@@ -5926,6 +6003,14 @@ async function getTerser() {
|
|
|
5926
6003
|
*
|
|
5927
6004
|
* Default: `false`
|
|
5928
6005
|
*
|
|
6006
|
+
* @prop {boolean} [collapseAttributeWhitespace]
|
|
6007
|
+
* Collapse multiple whitespace characters within attribute values into a
|
|
6008
|
+
* single space. Also trims leading and trailing whitespace from attribute
|
|
6009
|
+
* values. Applied as an early normalization step before special attribute
|
|
6010
|
+
* handlers (CSS minification, class sorting, etc.) run.
|
|
6011
|
+
*
|
|
6012
|
+
* Default: `false`
|
|
6013
|
+
*
|
|
5929
6014
|
* @prop {boolean} [collapseBooleanAttributes]
|
|
5930
6015
|
* Collapse boolean attributes to their name only (for example
|
|
5931
6016
|
* `disabled="disabled"` → `disabled`).
|
|
@@ -6611,6 +6696,12 @@ function isSrcset(attrName, tag) {
|
|
|
6611
6696
|
}
|
|
6612
6697
|
|
|
6613
6698
|
async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTMLSelf) {
|
|
6699
|
+
// Apply early whitespace normalization if enabled
|
|
6700
|
+
// Preserves special spaces (non-breaking space, hair space, etc.) for consistency with `collapseWhitespace`
|
|
6701
|
+
if (options.collapseAttributeWhitespace) {
|
|
6702
|
+
attrValue = attrValue.replace(/[ \n\r\t\f]+/g, ' ').replace(/^[ \n\r\t\f]+|[ \n\r\t\f]+$/g, '');
|
|
6703
|
+
}
|
|
6704
|
+
|
|
6614
6705
|
if (isEventAttribute(attrName, options)) {
|
|
6615
6706
|
attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, '');
|
|
6616
6707
|
return options.minifyJS(attrValue, true);
|
|
@@ -44,6 +44,15 @@ export type MinifierOptions = {
|
|
|
44
44
|
* Default: `false`
|
|
45
45
|
*/
|
|
46
46
|
caseSensitive?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Collapse multiple whitespace characters within attribute values into a
|
|
49
|
+
* single space. Also trims leading and trailing whitespace from attribute
|
|
50
|
+
* values. Applied as an early normalization step before special attribute
|
|
51
|
+
* handlers (CSS minification, class sorting, etc.) run.
|
|
52
|
+
*
|
|
53
|
+
* Default: `false`
|
|
54
|
+
*/
|
|
55
|
+
collapseAttributeWhitespace?: boolean;
|
|
47
56
|
/**
|
|
48
57
|
* Collapse boolean attributes to their name only (for example
|
|
49
58
|
* `disabled="disabled"` → `disabled`).
|
|
@@ -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":"AAkwEO,8BAJI,MAAM,YACN,eAAe,GACb,OAAO,CAAC,MAAM,CAAC,CAQ3B;;;;;;;;;;;;UAzuES,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;;;;;;;;;kCAON,OAAO;;;;;;;;gCAQR,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;;wBA3YkC,cAAc;0BAAd,cAAc;+BAAd,cAAc"}
|
|
@@ -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;AAuEpE;IACE,qCAGC;IAFC,UAAgB;IAChB,aAAsB;IAGxB,uBAoeC;CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../../src/presets.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../../src/presets.js"],"names":[],"mappings":"AAiDA;;;;GAIG;AACH,gCAHW,MAAM,GACJ,MAAM,GAAC,IAAI,CAMvB;AAED;;;GAGG;AACH,kCAFa,MAAM,EAAE,CAIpB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokenchain.d.ts","sourceRoot":"","sources":["../../src/tokenchain.js"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"tokenchain.d.ts","sourceRoot":"","sources":["../../src/tokenchain.js"],"names":[],"mappings":";AA2CA;IAGI,mBAAoB;IAGtB,uBAOC;IAED,uBAiDC;CACF;AA5GD;IACE,2CAuCC;CACF"}
|
package/package.json
CHANGED
|
@@ -15,17 +15,17 @@
|
|
|
15
15
|
},
|
|
16
16
|
"description": "Super-configurable, well-tested, JavaScript-based HTML minifier (enhanced successor of HTML Minifier)",
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@commitlint/cli": "^20.
|
|
18
|
+
"@commitlint/cli": "^20.2.0",
|
|
19
19
|
"@eslint/js": "^9.39.1",
|
|
20
20
|
"@rollup/plugin-commonjs": "^29.0.0",
|
|
21
21
|
"@rollup/plugin-json": "^6.1.0",
|
|
22
22
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
23
23
|
"@rollup/plugin-terser": "^0.4.4",
|
|
24
24
|
"eslint": "^9.39.1",
|
|
25
|
-
"rollup": "^4.53.
|
|
25
|
+
"rollup": "^4.53.3",
|
|
26
26
|
"rollup-plugin-polyfill-node": "^0.13.0",
|
|
27
27
|
"typescript": "^5.9.3",
|
|
28
|
-
"vite": "^7.2.
|
|
28
|
+
"vite": "^7.2.7"
|
|
29
29
|
},
|
|
30
30
|
"exports": {
|
|
31
31
|
".": {
|
|
@@ -84,5 +84,5 @@
|
|
|
84
84
|
"test:watch": "node --test --watch tests/*.spec.js"
|
|
85
85
|
},
|
|
86
86
|
"type": "module",
|
|
87
|
-
"version": "4.
|
|
87
|
+
"version": "4.11.0"
|
|
88
88
|
}
|
package/src/htmlminifier.js
CHANGED
|
@@ -61,6 +61,14 @@ async function getTerser() {
|
|
|
61
61
|
*
|
|
62
62
|
* Default: `false`
|
|
63
63
|
*
|
|
64
|
+
* @prop {boolean} [collapseAttributeWhitespace]
|
|
65
|
+
* Collapse multiple whitespace characters within attribute values into a
|
|
66
|
+
* single space. Also trims leading and trailing whitespace from attribute
|
|
67
|
+
* values. Applied as an early normalization step before special attribute
|
|
68
|
+
* handlers (CSS minification, class sorting, etc.) run.
|
|
69
|
+
*
|
|
70
|
+
* Default: `false`
|
|
71
|
+
*
|
|
64
72
|
* @prop {boolean} [collapseBooleanAttributes]
|
|
65
73
|
* Collapse boolean attributes to their name only (for example
|
|
66
74
|
* `disabled="disabled"` → `disabled`).
|
|
@@ -746,6 +754,12 @@ function isSrcset(attrName, tag) {
|
|
|
746
754
|
}
|
|
747
755
|
|
|
748
756
|
async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTMLSelf) {
|
|
757
|
+
// Apply early whitespace normalization if enabled
|
|
758
|
+
// Preserves special spaces (non-breaking space, hair space, etc.) for consistency with `collapseWhitespace`
|
|
759
|
+
if (options.collapseAttributeWhitespace) {
|
|
760
|
+
attrValue = attrValue.replace(/[ \n\r\t\f]+/g, ' ').replace(/^[ \n\r\t\f]+|[ \n\r\t\f]+$/g, '');
|
|
761
|
+
}
|
|
762
|
+
|
|
749
763
|
if (isEventAttribute(attrName, options)) {
|
|
750
764
|
attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, '');
|
|
751
765
|
return options.minifyJS(attrValue, true);
|