html-minifier-next 6.2.8 → 6.2.10
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 +38 -3
- package/cli.js +9 -4
- package/dist/htmlminifier.cjs +882 -557
- package/dist/types/htmlminifier.d.ts +77 -60
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/htmlparser.d.ts +19 -3
- package/dist/types/htmlparser.d.ts.map +1 -1
- package/dist/types/lib/attributes.d.ts +137 -23
- package/dist/types/lib/attributes.d.ts.map +1 -1
- package/dist/types/lib/content.d.ts +37 -5
- package/dist/types/lib/content.d.ts.map +1 -1
- package/dist/types/lib/elements.d.ts +29 -4
- package/dist/types/lib/elements.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts +17 -14
- package/dist/types/lib/options.d.ts.map +1 -1
- package/dist/types/lib/utils.d.ts +28 -11
- package/dist/types/lib/utils.d.ts.map +1 -1
- package/dist/types/lib/whitespace.d.ts +40 -6
- package/dist/types/lib/whitespace.d.ts.map +1 -1
- package/dist/types/tokenchain.d.ts +17 -3
- package/dist/types/tokenchain.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/htmlminifier.js +405 -346
- package/src/htmlparser.js +101 -48
- package/src/lib/attributes.js +166 -50
- package/src/lib/content.js +28 -12
- package/src/lib/elements.js +30 -7
- package/src/lib/options.js +55 -42
- package/src/lib/utils.js +26 -7
- package/src/lib/whitespace.js +30 -11
- package/src/presets.js +1 -1
- package/src/tokenchain.js +30 -17
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
|
-
|
|
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
|
-
'(' +
|
|
151
|
+
'(' + pair[0].source + ')\\s*' +
|
|
118
152
|
pattern +
|
|
119
|
-
'\\s*(' +
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
let
|
|
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
|
|
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(
|
|
665
|
+
await handler.end(entry.tag, entry.attrs, true);
|
|
614
666
|
}
|
|
615
667
|
}
|
|
616
668
|
stack.length = pos;
|
|
617
|
-
lastTag = pos
|
|
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]
|
|
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]
|
|
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
|
|
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) {
|