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.
@@ -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
- const attribute = attrForHandler(handler);
5276
- let last, prevTag = undefined, nextTag = undefined;
5277
-
5278
- // Track position for better error messages
5279
- let position = 0;
5280
- const getLineColumn = (pos) => {
5281
- const lines = this.html.slice(0, pos).split('\n');
5282
- return { line: lines.length, column: lines[lines.length - 1].length + 1 };
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 (html) {
5286
- last = html;
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
- html = html.substring(commentEnd + 3);
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
- html = html.substring(conditionalEnd + 2);
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
- html = html.substring(doctypeMatch[0].length);
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
- html = html.substring(endTagMatch[0].length);
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
- html = startTagMatch.rest;
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
- html = html.substring(textEnd);
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
- let nextTagMatch = parseStartTag(html);
5392
+ const nextHtml = remaining();
5393
+ let nextTagMatch = parseStartTag(nextHtml);
5365
5394
  if (nextTagMatch) {
5366
5395
  nextTag = nextTagMatch.tagName;
5367
5396
  } else {
5368
- nextTagMatch = html.match(endTag);
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
- html = html.slice(m.index + m[0].length);
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
- html = html.substring(1);
5432
+ advance(1);
5404
5433
  } else {
5405
5434
  break;
5406
5435
  }
5407
5436
  }
5408
5437
  }
5409
5438
 
5410
- if (html === last) {
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(html[0], prevTag, '');
5443
+ await handler.chars(fullHtml[pos], prevTag, '');
5415
5444
  }
5416
- html = html.substring(1);
5417
- position++;
5445
+ advance(1);
5418
5446
  prevTag = '';
5419
5447
  continue;
5420
5448
  }
5421
- const loc = getLineColumn(position);
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, position - CONTEXT_BEFORE);
5426
- const snippet = this.html.slice(startPos, startPos + 200).replace(/\n/g, ' ');
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}${this.html.length > startPos + 200 ? '…' : ''}`
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
- input = input.slice(start[0].length);
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
- input = input.slice(fullAttr.length);
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
- input = input.slice(attr[0].length);
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
- match.rest = input.slice(end[0].length);
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].tag.toLowerCase();
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].tag.toLowerCase() === needle) {
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 key = this.keys[i];
5731
- const token = key.slice(1);
5763
+ const token = this.keys[i];
5732
5764
 
5733
- let index = tokens.indexOf(token, fromIndex);
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 (index !== -1) {
5736
- do {
5737
- if (index !== fromIndex) {
5738
- tokens.splice(index, 1);
5739
- tokens.splice(fromIndex, 0, token);
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
- fromIndex++;
5742
- } while ((index = tokens.indexOf(token, fromIndex)) !== -1);
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
- return this[key].sort(tokens, fromIndex);
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
- const key = '$' + token;
5755
- if (!this[key]) {
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[key].push(tokens);
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
- sorter.keys = Object.keys(this).sort((j, k) => {
5767
- const m = this[j].length;
5768
- const n = this[k].length;
5769
- return m < n ? 1 : m > n ? -1 : j < k ? -1 : j > k ? 1 : 0;
5770
- }).filter((key) => {
5771
- if (this[key].processed < this[key].length) {
5772
- const token = key.slice(1);
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
- this[key].forEach((tokens) => {
5776
- let index;
5777
- while ((index = tokens.indexOf(token)) !== -1) {
5778
- tokens.splice(index, 1);
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
- tokens.forEach((token) => {
5781
- this['$' + token].processed++;
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
- chain.add(tokens.slice(0));
5855
+
5856
+ if (filtered.length > 0) {
5857
+ chain.add(filtered);
5858
+ }
5784
5859
  });
5785
- sorter[key] = chain.createSorter();
5786
- return true;
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;AAoEpE;IACE,qCAGC;IAFC,UAAgB;IAChB,aAAsB;IAGxB,uBAscC;CACF"}
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,5 +1,6 @@
1
1
  export default TokenChain;
2
2
  declare class TokenChain {
3
+ map: Map<any, any>;
3
4
  add(tokens: any): void;
4
5
  createSorter(): Sorter;
5
6
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tokenchain.d.ts","sourceRoot":"","sources":["../../src/tokenchain.js"],"names":[],"mappings":";AAwBA;IACE,uBASC;IAED,uBA4BC;CACF;AAjED;IACE,2CAoBC;CACF"}
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.1.0",
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.2",
25
+ "rollup": "^4.53.3",
26
26
  "rollup-plugin-polyfill-node": "^0.13.0",
27
27
  "typescript": "^5.9.3",
28
- "vite": "^7.2.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.9.2"
87
+ "version": "4.10.0"
88
88
  }