html-minifier-next 6.2.7 → 6.2.9

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/src/htmlparser.js CHANGED
@@ -7,6 +7,25 @@
7
7
 
8
8
  import { isThenable } from './lib/utils.js';
9
9
 
10
+ /** @import { HTMLAttribute } from './lib/attributes.js' */
11
+
12
+ // Type definitions
13
+
14
+ /**
15
+ * @typedef {{
16
+ * start?: Function,
17
+ * end?: Function,
18
+ * chars?: Function,
19
+ * comment?: Function,
20
+ * doctype?: Function,
21
+ * continueOnParseError?: boolean | undefined,
22
+ * partialMarkup?: boolean | undefined,
23
+ * wantsNextTag?: boolean | undefined,
24
+ * customAttrSurround?: RegExp[][] | undefined,
25
+ * customAttrAssign?: RegExp[] | undefined
26
+ * }} HTMLParserHandler
27
+ */
28
+
10
29
  /*
11
30
  * Use like so:
12
31
  *
@@ -19,7 +38,8 @@ import { isThenable } from './lib/utils.js';
19
38
  */
20
39
 
21
40
  class CaseInsensitiveSet extends Set {
22
- has(str) {
41
+ /** @override */
42
+ has(/** @type {string} */ str) {
23
43
  return super.has(str.toLowerCase());
24
44
  }
25
45
  }
@@ -54,8 +74,9 @@ const startTagOpen = new RegExp('^<' + qnameCapture);
54
74
  export const endTag = new RegExp('^</' + qnameCapture + '[^>]*>');
55
75
 
56
76
  let IS_REGEX_CAPTURING_BROKEN = false;
57
- 'x'.replace(/x(.)?/g, function (m, g) {
77
+ 'x'.replace(/x(.)?/g, function (/** @type {string} */ m, /** @type {string | undefined} */ g) {
58
78
  IS_REGEX_CAPTURING_BROKEN = g === '';
79
+ return m;
59
80
  });
60
81
 
61
82
  // Empty elements
@@ -89,6 +110,11 @@ const attrRegexCache = new WeakMap();
89
110
 
90
111
  // O(n) helper: Strip all occurrences of `open…close` delimiters, keeping inner content
91
112
  // Used instead of a regex replace to avoid O(n²) behavior on adversarial inputs
113
+ /**
114
+ * @param {string} str
115
+ * @param {string} open
116
+ * @param {string} close
117
+ */
92
118
  function stripDelimited(str, open, close) {
93
119
  let result = '';
94
120
  let i = 0;
@@ -104,6 +130,7 @@ function stripDelimited(str, open, close) {
104
130
  return result;
105
131
  }
106
132
 
133
+ /** @param {HTMLParserHandler} handler */
107
134
  function buildAttrRegex(handler) {
108
135
  const unquotedValue = handler.continueOnParseError ? singleAttrValueLenientUnquoted : singleAttrValues[2];
109
136
  const attrValues = [singleAttrValues[0], singleAttrValues[1], unquotedValue];
@@ -111,12 +138,19 @@ function buildAttrRegex(handler) {
111
138
  '(?:\\s*(' + joinSingleAttrAssigns(handler) + ')' +
112
139
  '[ \\t\\n\\f\\r]*(?:' + attrValues.join('|') + '))?';
113
140
  if (handler.customAttrSurround) {
141
+ if (!Array.isArray(handler.customAttrSurround)) {
142
+ throw new Error('`customAttrSurround` must be an array of `[RegExp, RegExp]` pairs');
143
+ }
114
144
  const attrClauses = [];
115
145
  for (let i = handler.customAttrSurround.length - 1; i >= 0; i--) {
146
+ const pair = handler.customAttrSurround[i];
147
+ if (!Array.isArray(pair) || !(pair[0] instanceof RegExp) || !(pair[1] instanceof RegExp)) {
148
+ throw new Error('`customAttrSurround` entries must be `[RegExp, RegExp]` pairs');
149
+ }
116
150
  attrClauses[i] = '(?:' +
117
- '(' + handler.customAttrSurround[i][0].source + ')\\s*' +
151
+ '(' + pair[0].source + ')\\s*' +
118
152
  pattern +
119
- '\\s*(' + handler.customAttrSurround[i][1].source + ')' +
153
+ '\\s*(' + pair[1].source + ')' +
120
154
  ')';
121
155
  }
122
156
  attrClauses.push('(?:' + pattern + ')');
@@ -125,6 +159,7 @@ function buildAttrRegex(handler) {
125
159
  return new RegExp('^\\s*' + pattern);
126
160
  }
127
161
 
162
+ /** @param {HTMLParserHandler} handler */
128
163
  function getAttrRegexForHandler(handler) {
129
164
  let cached = attrRegexCache.get(handler);
130
165
  if (cached) return cached;
@@ -136,6 +171,7 @@ function getAttrRegexForHandler(handler) {
136
171
  // Cache for sticky attribute regexes (`y` flag for position-based matching on full string)
137
172
  const attrRegexStickyCache = new WeakMap();
138
173
 
174
+ /** @param {HTMLParserHandler} handler */
139
175
  function getAttrRegexStickyForHandler(handler) {
140
176
  let cached = attrRegexStickyCache.get(handler);
141
177
  if (cached) return cached;
@@ -146,6 +182,7 @@ function getAttrRegexStickyForHandler(handler) {
146
182
  return compiled;
147
183
  }
148
184
 
185
+ /** @param {HTMLParserHandler} handler */
149
186
  function joinSingleAttrAssigns(handler) {
150
187
  return singleAttrAssigns.concat(
151
188
  handler.customAttrAssign || []
@@ -158,6 +195,10 @@ function joinSingleAttrAssigns(handler) {
158
195
  const NCP = 7;
159
196
 
160
197
  export class HTMLParser {
198
+ /**
199
+ * @param {string} html
200
+ * @param {HTMLParserHandler} handler
201
+ */
161
202
  constructor(html, handler) {
162
203
  this.html = html;
163
204
  this.handler = handler;
@@ -168,12 +209,21 @@ export class HTMLParser {
168
209
  const fullHtml = this.html;
169
210
  const fullLength = fullHtml.length;
170
211
 
171
- const stack = []; let lastTag;
212
+ /** @type {Array<{tag: string, lowerTag: string, attrs: HTMLAttribute[]}>} */
213
+ const stack = [];
214
+ /** @type {string} */
215
+ let lastTag = '';
172
216
  // Use cached attribute regex for this handler configuration
173
217
  const attribute = getAttrRegexForHandler(handler);
174
218
  const attributeY = getAttrRegexStickyForHandler(handler);
175
- let prevTag = undefined, nextTag = undefined;
176
- let prevAttrs = [], nextAttrs = [];
219
+ /** @type {string | undefined} */
220
+ let prevTag;
221
+ /** @type {string | undefined} */
222
+ let nextTag;
223
+ /** @type {HTMLAttribute[]} */
224
+ let prevAttrs = [];
225
+ /** @type {HTMLAttribute[]} */
226
+ let nextAttrs = [];
177
227
 
178
228
  // Sticky regex versions for position-based matching (avoids string slicing)
179
229
  const startTagOpenY = new RegExp(startTagOpen.source.slice(1), 'y');
@@ -193,10 +243,10 @@ export class HTMLParser {
193
243
  let lastPos;
194
244
 
195
245
  // Helper to advance position
196
- const advance = (n) => { pos += n; };
246
+ const advance = (/** @type {number} */ n) => { pos += n; };
197
247
 
198
248
  // Lazy line/column calculation—only compute on actual errors
199
- const getLineColumn = (position) => {
249
+ const getLineColumn = (/** @type {number} */ position) => {
200
250
  let line = 1;
201
251
  let column = 1;
202
252
  for (let i = 0; i < position; i++) {
@@ -211,7 +261,7 @@ export class HTMLParser {
211
261
  };
212
262
 
213
263
  // Helper to safely extract substring when needed for stacked tag content
214
- const sliceFromPos = (startPos) => {
264
+ const sliceFromPos = (/** @type {number} */ startPos) => {
215
265
  return fullHtml.slice(startPos);
216
266
  };
217
267
 
@@ -239,9 +289,9 @@ export class HTMLParser {
239
289
  const endTagMatch = cachedNextEndTag.match;
240
290
  cachedNextStartTag = null;
241
291
  cachedNextEndTag = null;
242
- advance(endTagMatch[0].length);
243
- await parseEndTag(endTagMatch[0], endTagMatch[1]);
244
- prevTag = '/' + endTagMatch[1].toLowerCase();
292
+ advance((endTagMatch[0] ?? '').length);
293
+ await parseEndTag(endTagMatch[0] ?? '', endTagMatch[1] ?? '');
294
+ prevTag = '/' + (endTagMatch[1] ?? '').toLowerCase();
245
295
  prevAttrs = [];
246
296
  continue;
247
297
  }
@@ -299,9 +349,9 @@ export class HTMLParser {
299
349
  endTagY.lastIndex = pos;
300
350
  const endTagMatch = endTagY.exec(fullHtml);
301
351
  if (endTagMatch) {
302
- advance(endTagMatch[0].length);
303
- await parseEndTag(endTagMatch[0], endTagMatch[1]);
304
- prevTag = '/' + endTagMatch[1].toLowerCase();
352
+ advance((endTagMatch[0] ?? '').length);
353
+ await parseEndTag(endTagMatch[0] ?? '', endTagMatch[1] ?? '');
354
+ prevTag = '/' + (endTagMatch[1] ?? '').toLowerCase();
305
355
  prevAttrs = [];
306
356
  continue;
307
357
  }
@@ -361,12 +411,12 @@ export class HTMLParser {
361
411
  } else {
362
412
  const stackedTag = lastTag.toLowerCase();
363
413
  // Use pre-compiled regex for common tags (`script`, `style`, `noscript`) to avoid regex creation overhead
364
- const reStackedTag = preCompiledStackedTags[stackedTag] || reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)\\x3c/' + stackedTag + '[^>]*>', 'i'));
414
+ const reStackedTag = /** @type {Record<string, RegExp>} */ (preCompiledStackedTags)[stackedTag] || /** @type {Record<string, RegExp>} */ (reCache)[stackedTag] || (/** @type {Record<string, RegExp>} */ (reCache)[stackedTag] = new RegExp('([\\s\\S]*?)\\x3c/' + stackedTag + '[^>]*>', 'i'));
365
415
 
366
416
  const remaining = sliceFromPos(pos);
367
417
  const m = reStackedTag.exec(remaining);
368
418
  if (m && m.index === 0) {
369
- let text = m[1];
419
+ let text = m[1] ?? '';
370
420
  if (stackedTag !== 'script' && stackedTag !== 'style' && stackedTag !== 'noscript') {
371
421
  text = stripDelimited(stripDelimited(text, '<!--', '-->'), '<![CDATA[', ']]>');
372
422
  }
@@ -406,40 +456,39 @@ export class HTMLParser {
406
456
  const CONTEXT_BEFORE = 50;
407
457
  const startPos = Math.max(0, pos - CONTEXT_BEFORE);
408
458
  const snippet = fullHtml.slice(startPos, startPos + 200).replace(/\n/g, ' ');
409
- throw new Error(
410
- `Parse error at line ${loc.line}, column ${loc.column}:\n${snippet}${fullHtml.length > startPos + 200 ? '…' : ''}`
411
- );
459
+ throw new Error(`Parse error at line ${loc.line}, column ${loc.column}:\n${snippet}${fullHtml.length > startPos + 200 ? '…' : ''}`);
412
460
  }
413
461
  }
414
462
 
415
463
  if (!handler.partialMarkup) {
416
464
  // Clean up any remaining tags
417
- await parseEndTag();
465
+ await parseEndTag('', '');
418
466
  }
419
467
 
420
468
  // Helper to extract minimal attribute info (name/value pairs) from raw attribute matches
421
469
  // Used for whitespace collapsing logic—doesn’t need full processing
422
- function extractAttrInfo(rawAttrs) {
470
+ function extractAttrInfo(/** @type {Array<Array<string | undefined>>} */ rawAttrs) {
423
471
  if (!rawAttrs || !rawAttrs.length) return [];
424
472
 
425
473
  const numCustomParts = handler.customAttrSurround ? handler.customAttrSurround.length * NCP : 0;
426
474
  const baseIndex = 1 + numCustomParts;
427
475
 
428
- return rawAttrs.map(args => {
476
+ return /** @type {HTMLAttribute[]} */ (rawAttrs.map((/** @type {Array<string | undefined>} */ args) => {
429
477
  // Extract attribute name (always at `baseIndex`)
430
478
  const name = args[baseIndex];
431
479
  // Extract value from double-quoted (`baseIndex + 2`), single-quoted (`baseIndex + 3`), or unquoted (`baseIndex + 4`)
432
480
  const value = args[baseIndex + 2] ?? args[baseIndex + 3] ?? args[baseIndex + 4];
433
- return { name: name?.toLowerCase(), value };
434
- }).filter(attr => attr.name); // Filter out invalid entries
481
+ return { name: name?.toLowerCase() ?? '', value };
482
+ }).filter(attr => attr.name)); // Filter out invalid entries
435
483
  }
436
484
 
437
- function parseStartTag(startPos) {
485
+ function parseStartTag(/** @type {number} */ startPos) {
438
486
  startTagOpenY.lastIndex = startPos;
439
487
  const start = startTagOpenY.exec(fullHtml);
440
488
  if (start) {
489
+ /** @type {{tagName: string, attrs: Array<Array<string|undefined>>, advance: number, unarySlash?: string}} */
441
490
  const match = {
442
- tagName: start[1],
491
+ tagName: start[1] ?? '',
443
492
  attrs: [],
444
493
  advance: 0
445
494
  };
@@ -582,19 +631,21 @@ export class HTMLParser {
582
631
  startTagCloseY.lastIndex = currentPos;
583
632
  end = startTagCloseY.exec(fullHtml);
584
633
  if (end) {
585
- match.unarySlash = end[1];
586
- consumed += end[0].length;
634
+ match.unarySlash = end[1] ?? '';
635
+ consumed += (end[0] ?? '').length;
587
636
  match.advance = consumed;
588
637
  return match;
589
638
  }
590
639
  }
640
+ return undefined;
591
641
  }
592
642
 
593
- function findTagInCurrentTable(tagName) {
643
+ function findTagInCurrentTable(/** @type {string} */ tagName) {
594
644
  let pos;
595
645
  const needle = tagName.toLowerCase();
596
646
  for (pos = stack.length - 1; pos >= 0; pos--) {
597
- const currentTag = stack[pos].lowerTag;
647
+ const entry = stack[pos];
648
+ const currentTag = entry?.lowerTag;
598
649
  if (currentTag === needle) {
599
650
  return pos;
600
651
  }
@@ -606,18 +657,19 @@ export class HTMLParser {
606
657
  return -1;
607
658
  }
608
659
 
609
- async function parseEndTagAt(pos) {
660
+ async function parseEndTagAt(/** @type {number} */ pos) {
610
661
  // Close all open elements up to `pos` (mirrors `parseEndTag`’s core branch)
611
662
  for (let i = stack.length - 1; i >= pos; i--) {
663
+ const entry = /** @type {NonNullable<(typeof stack)[number]>} */ (stack[i]);
612
664
  if (handler.end) {
613
- await handler.end(stack[i].tag, stack[i].attrs, true);
665
+ await handler.end(entry.tag, entry.attrs, true);
614
666
  }
615
667
  }
616
668
  stack.length = pos;
617
- lastTag = pos && stack[pos - 1].tag;
669
+ lastTag = pos ? (stack[pos - 1]?.tag ?? '') : '';
618
670
  }
619
671
 
620
- async function closeIfFoundInCurrentTable(tagName) {
672
+ async function closeIfFoundInCurrentTable(/** @type {string} */ tagName) {
621
673
  const pos = findTagInCurrentTable(tagName);
622
674
  if (pos >= 0) {
623
675
  // Close at the specific index to avoid re-searching
@@ -627,7 +679,7 @@ export class HTMLParser {
627
679
  return false;
628
680
  }
629
681
 
630
- async function handleStartTag(match) {
682
+ async function handleStartTag(/** @type {{tagName: string, attrs: Array<Array<string | undefined>>, advance: number, unarySlash?: string}} */ match) {
631
683
  const tagName = match.tagName;
632
684
  let unarySlash = match.unarySlash;
633
685
 
@@ -669,17 +721,18 @@ export class HTMLParser {
669
721
 
670
722
  const unary = empty.has(tagName) || (tagName === 'html' && lastTag === 'head') || !!unarySlash;
671
723
 
672
- const attrs = match.attrs.map(function (args) {
724
+ const attrs = /** @type {HTMLAttribute[]} */ (match.attrs.map(function (/** @type {Array<string | undefined>} */ args) {
725
+ /** @type {string | undefined} */
673
726
  let name, value, customOpen, customClose, customAssign, quote;
674
727
 
675
728
  // Hackish workaround for Firefox bug, https://bugzilla.mozilla.org/show_bug.cgi?id=369778
676
- if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
729
+ if (IS_REGEX_CAPTURING_BROKEN && (args[0] ?? '').indexOf('""') === -1) {
677
730
  if (args[3] === '') { delete args[3]; }
678
731
  if (args[4] === '') { delete args[4]; }
679
732
  if (args[5] === '') { delete args[5]; }
680
733
  }
681
734
 
682
- function populate(index) {
735
+ function populate(/** @type {number} */ index) {
683
736
  customAssign = args[index];
684
737
  value = args[index + 1];
685
738
  if (typeof value !== 'undefined') {
@@ -690,7 +743,7 @@ export class HTMLParser {
690
743
  return '\'';
691
744
  }
692
745
  value = args[index + 3];
693
- if (typeof value === 'undefined' && fillAttrs.has(name)) {
746
+ if (typeof value === 'undefined' && name && fillAttrs.has(name)) {
694
747
  value = name;
695
748
  }
696
749
  return '';
@@ -714,14 +767,14 @@ export class HTMLParser {
714
767
  }
715
768
 
716
769
  return {
717
- name,
770
+ name: name ?? '',
718
771
  value,
719
772
  customAssign: customAssign || '=',
720
773
  customOpen: customOpen || '',
721
774
  customClose: customClose || '',
722
775
  quote: quote || ''
723
776
  };
724
- });
777
+ }));
725
778
 
726
779
  if (!unary) {
727
780
  stack.push({ tag: tagName, lowerTag: tagName.toLowerCase(), attrs });
@@ -737,18 +790,18 @@ export class HTMLParser {
737
790
  }
738
791
  }
739
792
 
740
- function findTag(tagName) {
793
+ function findTag(/** @type {string} */ tagName) {
741
794
  let pos;
742
795
  const needle = tagName.toLowerCase();
743
796
  for (pos = stack.length - 1; pos >= 0; pos--) {
744
- if (stack[pos].lowerTag === needle) {
797
+ if (stack[pos]?.lowerTag === needle) {
745
798
  break;
746
799
  }
747
800
  }
748
801
  return pos;
749
802
  }
750
803
 
751
- async function parseEndTag(tag, tagName) {
804
+ async function parseEndTag(/** @type {string} */ tag, /** @type {string} */ tagName) {
752
805
  let pos;
753
806
 
754
807
  // Find the closest opened tag of the same type
@@ -762,13 +815,13 @@ export class HTMLParser {
762
815
  // Close all the open elements, up the stack
763
816
  for (let i = stack.length - 1; i >= pos; i--) {
764
817
  if (handler.end) {
765
- handler.end(stack[i].tag, stack[i].attrs, i > pos || !tag);
818
+ handler.end(stack[i]?.tag, stack[i]?.attrs, i > pos || !tag);
766
819
  }
767
820
  }
768
821
 
769
822
  // Remove the open elements from the stack
770
823
  stack.length = pos;
771
- lastTag = pos && stack[pos - 1].tag;
824
+ lastTag = pos ? (stack[pos - 1]?.tag ?? '') : '';
772
825
  } else if (handler.partialMarkup && tagName) {
773
826
  // In partial markup mode, preserve stray end tags
774
827
  if (handler.end) {