html-minifier-next 4.9.2 → 4.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -18
- package/dist/htmlminifier.cjs +151 -75
- package/dist/htmlminifier.esm.bundle.js +151 -75
- package/dist/types/htmlparser.d.ts.map +1 -1
- package/dist/types/tokenchain.d.ts +1 -0
- package/dist/types/tokenchain.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/htmlparser.js +75 -42
- 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
|
-
|
|
5788
|
+
}
|
|
5789
|
+
|
|
5790
|
+
// Copy sorted portion back to tokens array
|
|
5791
|
+
for (let j = 0; j < result.length; j++) {
|
|
5792
|
+
tokens[fromIndex + j] = result[j];
|
|
5793
|
+
}
|
|
5743
5794
|
|
|
5744
|
-
|
|
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();
|
|
5765
5821
|
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
const
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
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
|
+
});
|
|
5832
|
+
|
|
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
|
}
|
|
@@ -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":"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.10.0"
|
|
88
88
|
}
|