Skip to content

Commit 55695db

Browse files
committed
simplify
1 parent 69c1b35 commit 55695db

4 files changed

Lines changed: 230 additions & 24 deletions

File tree

src/Parser.events.spec.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -199,18 +199,22 @@ describe("Events", () => {
199199
runTest("<svg><foreignObject><b>x</b></foreignObject></svg>"));
200200

201201
it("SVG integration point closing with unclosed child", () =>
202-
runTest(
203-
"<svg><foreignObject><div>x</foreignObject></svg>after",
204-
));
202+
runTest("<svg><foreignObject><div>x</foreignObject></svg>after"));
205203

206204
it("Content after SVG integration point", () =>
207-
runTest(
208-
"<svg><foreignObject><b>x</b></foreignObject><rect/></svg>",
209-
));
205+
runTest("<svg><foreignObject><b>x</b></foreignObject><rect/></svg>"));
210206

211207
it("Stray </svg> does not break foreign context", () =>
212208
runTest("</svg><script><b>not a tag</b></script>"));
213209

210+
it("Implicit close of nested foreign elements", () =>
211+
runTest("<svg><math><mi>text</svg><script><b>x</b></script>"));
212+
213+
it("Self-closing foreign element with recognizeSelfClosing", () =>
214+
runTest("<svg/><script><b>x</b></script>", {
215+
recognizeSelfClosing: true,
216+
}));
217+
214218
it("HTML image alias", () => runTest("<image></image>"));
215219

216220
it("SVG image is not aliased", () => runTest("<svg><image></image></svg>"));

src/Parser.ts

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -467,22 +467,10 @@ export class Parser implements Callbacks {
467467
if (!this.isVoidElement(name)) {
468468
const pos = this.stack.indexOf(name);
469469
if (pos !== -1) {
470-
// Only shift foreignContext when the element is actually on
471-
// the stack — a stray </svg> must not underflow the stack.
472-
if (
473-
this.htmlMode &&
474-
(foreignContextElements.has(name) ||
475-
htmlIntegrationElements.has(name))
476-
) {
477-
this.foreignContext.shift();
478-
}
479-
for (let index = 0; index <= pos; index++) {
480-
const element = this.stack.shift();
481-
if (element === undefined) {
482-
break;
483-
}
484-
this.cbs.onclosetag?.(element, index !== pos);
470+
for (let index = 0; index < pos; index++) {
471+
this.popElement(true);
485472
}
473+
this.popElement(false);
486474
} else if (this.htmlMode && name === "p") {
487475
// Implicit open before close
488476
this.emitOpenTag("p");
@@ -516,15 +504,31 @@ export class Parser implements Callbacks {
516504
}
517505
}
518506

507+
/**
508+
* Pop the top element off the stack, emit a close event, and maintain
509+
* the foreign context stack.
510+
* @param implied Whether this close is implied (not from an explicit end tag).
511+
*/
512+
private popElement(implied: boolean): void {
513+
// biome-ignore lint/style/noNonNullAssertion: The element is guaranteed to exist.
514+
const element = this.stack.shift()!;
515+
if (
516+
this.htmlMode &&
517+
(foreignContextElements.has(element) ||
518+
htmlIntegrationElements.has(element))
519+
) {
520+
this.foreignContext.shift();
521+
}
522+
this.cbs.onclosetag?.(element, implied);
523+
}
524+
519525
private closeCurrentTag(isOpenImplied: boolean) {
520526
const name = this.tagname;
521527
this.endOpenTag(isOpenImplied);
522528

523529
// Self-closing tags will be on the top of the stack
524530
if (this.stack[0] === name) {
525-
// If the opening tag isn't implied, the closing tag has to be implied.
526-
this.cbs.onclosetag?.(name, !isOpenImplied);
527-
this.stack.shift();
531+
this.popElement(!isOpenImplied);
528532
}
529533
}
530534

src/Tokenizer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,8 @@ export default class Tokenizer {
899899
break;
900900
}
901901
case State.InPlainText: {
902+
// Skip to end of buffer; cleanup() emits the text.
903+
this.index = this.buffer.length + this.offset - 1;
902904
break;
903905
}
904906
case State.SpecialStartSequence: {

src/__snapshots__/Parser.events.spec.ts.snap

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,135 @@ exports[`Events > HTML image alias 1`] = `
962962
]
963963
`;
964964
965+
exports[`Events > Implicit close of nested foreign elements 1`] = `
966+
[
967+
{
968+
"$event": "opentagname",
969+
"data": [
970+
"svg",
971+
],
972+
"endIndex": 4,
973+
"startIndex": 0,
974+
},
975+
{
976+
"$event": "opentag",
977+
"data": [
978+
"svg",
979+
{},
980+
false,
981+
],
982+
"endIndex": 4,
983+
"startIndex": 0,
984+
},
985+
{
986+
"$event": "opentagname",
987+
"data": [
988+
"math",
989+
],
990+
"endIndex": 10,
991+
"startIndex": 5,
992+
},
993+
{
994+
"$event": "opentag",
995+
"data": [
996+
"math",
997+
{},
998+
false,
999+
],
1000+
"endIndex": 10,
1001+
"startIndex": 5,
1002+
},
1003+
{
1004+
"$event": "opentagname",
1005+
"data": [
1006+
"mi",
1007+
],
1008+
"endIndex": 14,
1009+
"startIndex": 11,
1010+
},
1011+
{
1012+
"$event": "opentag",
1013+
"data": [
1014+
"mi",
1015+
{},
1016+
false,
1017+
],
1018+
"endIndex": 14,
1019+
"startIndex": 11,
1020+
},
1021+
{
1022+
"$event": "text",
1023+
"data": [
1024+
"text",
1025+
],
1026+
"endIndex": 18,
1027+
"startIndex": 15,
1028+
},
1029+
{
1030+
"$event": "closetag",
1031+
"data": [
1032+
"mi",
1033+
true,
1034+
],
1035+
"endIndex": 24,
1036+
"startIndex": 19,
1037+
},
1038+
{
1039+
"$event": "closetag",
1040+
"data": [
1041+
"math",
1042+
true,
1043+
],
1044+
"endIndex": 24,
1045+
"startIndex": 19,
1046+
},
1047+
{
1048+
"$event": "closetag",
1049+
"data": [
1050+
"svg",
1051+
false,
1052+
],
1053+
"endIndex": 24,
1054+
"startIndex": 19,
1055+
},
1056+
{
1057+
"$event": "opentagname",
1058+
"data": [
1059+
"script",
1060+
],
1061+
"endIndex": 32,
1062+
"startIndex": 25,
1063+
},
1064+
{
1065+
"$event": "opentag",
1066+
"data": [
1067+
"script",
1068+
{},
1069+
false,
1070+
],
1071+
"endIndex": 32,
1072+
"startIndex": 25,
1073+
},
1074+
{
1075+
"$event": "text",
1076+
"data": [
1077+
"<b>x</b>",
1078+
],
1079+
"endIndex": 40,
1080+
"startIndex": 33,
1081+
},
1082+
{
1083+
"$event": "closetag",
1084+
"data": [
1085+
"script",
1086+
false,
1087+
],
1088+
"endIndex": 49,
1089+
"startIndex": 41,
1090+
},
1091+
]
1092+
`;
1093+
9651094
exports[`Events > Implicit close tags 1`] = `
9661095
[
9671096
{
@@ -2638,6 +2767,73 @@ exports[`Events > Scripts ending with < 1`] = `
26382767
]
26392768
`;
26402769
2770+
exports[`Events > Self-closing foreign element with recognizeSelfClosing 1`] = `
2771+
[
2772+
{
2773+
"$event": "opentagname",
2774+
"data": [
2775+
"svg",
2776+
],
2777+
"endIndex": 4,
2778+
"startIndex": 0,
2779+
},
2780+
{
2781+
"$event": "opentag",
2782+
"data": [
2783+
"svg",
2784+
{},
2785+
false,
2786+
],
2787+
"endIndex": 5,
2788+
"startIndex": 0,
2789+
},
2790+
{
2791+
"$event": "closetag",
2792+
"data": [
2793+
"svg",
2794+
true,
2795+
],
2796+
"endIndex": 5,
2797+
"startIndex": 0,
2798+
},
2799+
{
2800+
"$event": "opentagname",
2801+
"data": [
2802+
"script",
2803+
],
2804+
"endIndex": 13,
2805+
"startIndex": 6,
2806+
},
2807+
{
2808+
"$event": "opentag",
2809+
"data": [
2810+
"script",
2811+
{},
2812+
false,
2813+
],
2814+
"endIndex": 13,
2815+
"startIndex": 6,
2816+
},
2817+
{
2818+
"$event": "text",
2819+
"data": [
2820+
"<b>x</b>",
2821+
],
2822+
"endIndex": 21,
2823+
"startIndex": 14,
2824+
},
2825+
{
2826+
"$event": "closetag",
2827+
"data": [
2828+
"script",
2829+
false,
2830+
],
2831+
"endIndex": 30,
2832+
"startIndex": 22,
2833+
},
2834+
]
2835+
`;
2836+
26412837
exports[`Events > Self-closing indices (#941) 1`] = `
26422838
[
26432839
{

0 commit comments

Comments
 (0)