From b0624bf353180231c181003558d403531b1f2fb5 Mon Sep 17 00:00:00 2001 From: maximilliangrand <214999687+maximilliangrand@users.noreply.github.com> Date: Sat, 15 Aug 2026 12:14:43 +0200 Subject: [PATCH 1/2] fix: acknowledge self-closing flag on foreign integration points In HTML mode the self-closing flag is honored for foreign (SVG/MathML) elements, e.g. `x` closes `rect` and keeps `x` as a sibling. Integration points such as ``, `` inside `<svg>`, or `<mi>` inside `<math>` are themselves foreign elements, but opening them pushes an HTML child context, so `isInForeignContext()` returned false and `onselfclosingtag` ignored the slash, nesting the following content inside the element: parseDocument("<svg><foreignObject/>x</svg>") // before: foreignObject contains "x" (wrong) // after: foreignObject is empty, "x" is a sibling (matches parse5) Decide the acknowledgement from whether the current element is itself foreign (checking the enclosing context for integration points) rather than from the child context it establishes. --- src/Parser.events.spec.ts | 9 + src/Parser.ts | 21 +- src/__snapshots__/Parser.events.spec.ts.snap | 201 +++++++++++++++++++ 3 files changed, 230 insertions(+), 1 deletion(-) diff --git a/src/Parser.events.spec.ts b/src/Parser.events.spec.ts index 40c4c85e0..45f951e25 100644 --- a/src/Parser.events.spec.ts +++ b/src/Parser.events.spec.ts @@ -215,6 +215,15 @@ describe("Events", () => { recognizeSelfClosing: true, })); + it("Self-closing SVG integration point acknowledges the slash", () => + runTest("<svg><foreignObject/>x</svg>")); + + it("Self-closing SVG title acknowledges the slash", () => + runTest("<svg><title/>x</svg>")); + + it("Self-closing MathML integration point acknowledges the slash", () => + runTest("<math><mi/>x</math>")); + it("HTML image alias", () => runTest("<image></image>")); it("SVG image is not aliased", () => runTest("<svg><image></image></svg>")); diff --git a/src/Parser.ts b/src/Parser.ts index 33050243b..174a231ff 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -338,6 +338,25 @@ export class Parser implements Callbacks { return this.foreignContext[0] !== ForeignContext.None; } + /** + * Whether the element currently being opened is a foreign (SVG/MathML) + * element. Foreign elements acknowledge the self-closing flag even in HTML + * mode. + * + * HTML integration points (e.g. `<foreignObject>`, or `<title>` inside + * `<svg>`) switch their *children* back to the HTML context, so + * `isInForeignContext()` reports `false` for them even though the element + * itself is foreign. For those, check the enclosing context instead. + */ + private currentTagIsForeign(): boolean { + return ( + this.isInForeignContext() || + (this.htmlMode && + htmlIntegrationElements.has(this.tagname) && + this.foreignContext[1] !== ForeignContext.None) + ); + } + /** * Checks if the current tag is a void element. Override this if you want * to specify your own additional void elements. @@ -503,7 +522,7 @@ export class Parser implements Callbacks { */ onselfclosingtag(endIndex: number): void { this.endIndex = endIndex; - if (this.recognizeSelfClosing || this.isInForeignContext()) { + if (this.recognizeSelfClosing || this.currentTagIsForeign()) { this.closeCurrentTag(false); // Set `startIndex` for next node diff --git a/src/__snapshots__/Parser.events.spec.ts.snap b/src/__snapshots__/Parser.events.spec.ts.snap index 686fcafcc..9c33556a2 100644 --- a/src/__snapshots__/Parser.events.spec.ts.snap +++ b/src/__snapshots__/Parser.events.spec.ts.snap @@ -2767,6 +2767,207 @@ exports[`Events > Scripts ending with < 1`] = ` ] `; +exports[`Events > Self-closing MathML integration point acknowledges the slash 1`] = ` +[ + { + "$event": "opentagname", + "data": [ + "math", + ], + "endIndex": 5, + "startIndex": 0, + }, + { + "$event": "opentag", + "data": [ + "math", + {}, + false, + ], + "endIndex": 5, + "startIndex": 0, + }, + { + "$event": "opentagname", + "data": [ + "mi", + ], + "endIndex": 9, + "startIndex": 6, + }, + { + "$event": "opentag", + "data": [ + "mi", + {}, + false, + ], + "endIndex": 10, + "startIndex": 6, + }, + { + "$event": "closetag", + "data": [ + "mi", + true, + ], + "endIndex": 10, + "startIndex": 6, + }, + { + "$event": "text", + "data": [ + "x", + ], + "endIndex": 11, + "startIndex": 11, + }, + { + "$event": "closetag", + "data": [ + "math", + false, + ], + "endIndex": 18, + "startIndex": 12, + }, +] +`; + +exports[`Events > Self-closing SVG integration point acknowledges the slash 1`] = ` +[ + { + "$event": "opentagname", + "data": [ + "svg", + ], + "endIndex": 4, + "startIndex": 0, + }, + { + "$event": "opentag", + "data": [ + "svg", + {}, + false, + ], + "endIndex": 4, + "startIndex": 0, + }, + { + "$event": "opentagname", + "data": [ + "foreignObject", + ], + "endIndex": 19, + "startIndex": 5, + }, + { + "$event": "opentag", + "data": [ + "foreignObject", + {}, + false, + ], + "endIndex": 20, + "startIndex": 5, + }, + { + "$event": "closetag", + "data": [ + "foreignObject", + true, + ], + "endIndex": 20, + "startIndex": 5, + }, + { + "$event": "text", + "data": [ + "x", + ], + "endIndex": 21, + "startIndex": 21, + }, + { + "$event": "closetag", + "data": [ + "svg", + false, + ], + "endIndex": 27, + "startIndex": 22, + }, +] +`; + +exports[`Events > Self-closing SVG title acknowledges the slash 1`] = ` +[ + { + "$event": "opentagname", + "data": [ + "svg", + ], + "endIndex": 4, + "startIndex": 0, + }, + { + "$event": "opentag", + "data": [ + "svg", + {}, + false, + ], + "endIndex": 4, + "startIndex": 0, + }, + { + "$event": "opentagname", + "data": [ + "title", + ], + "endIndex": 11, + "startIndex": 5, + }, + { + "$event": "opentag", + "data": [ + "title", + {}, + false, + ], + "endIndex": 12, + "startIndex": 5, + }, + { + "$event": "closetag", + "data": [ + "title", + true, + ], + "endIndex": 12, + "startIndex": 5, + }, + { + "$event": "text", + "data": [ + "x", + ], + "endIndex": 13, + "startIndex": 13, + }, + { + "$event": "closetag", + "data": [ + "svg", + false, + ], + "endIndex": 19, + "startIndex": 14, + }, +] +`; + exports[`Events > Self-closing foreign element with recognizeSelfClosing 1`] = ` [ { From 36736f56d29080b5b8594903345f7e6a2f6f4202 Mon Sep 17 00:00:00 2001 From: maximilliangrand <214999687+maximilliangrand@users.noreply.github.com> Date: Sun, 16 Aug 2026 10:40:34 +0200 Subject: [PATCH 2/2] test: cover SVG desc, MathML annotation-xml, and a negative HTML-descendant case --- src/Parser.events.spec.ts | 13 ++ src/__snapshots__/Parser.events.spec.ts.snap | 228 +++++++++++++++++++ 2 files changed, 241 insertions(+) diff --git a/src/Parser.events.spec.ts b/src/Parser.events.spec.ts index 45f951e25..17d761e8e 100644 --- a/src/Parser.events.spec.ts +++ b/src/Parser.events.spec.ts @@ -221,9 +221,22 @@ describe("Events", () => { it("Self-closing SVG title acknowledges the slash", () => runTest("<svg><title/>x</svg>")); + it("Self-closing SVG desc acknowledges the slash", () => + runTest("<svg><desc/>x</svg>")); + it("Self-closing MathML integration point acknowledges the slash", () => runTest("<math><mi/>x</math>")); + it("Self-closing MathML annotation-xml acknowledges the slash", () => + runTest("<math><annotation-xml/>x</math>")); + + it("Self-closing HTML descendant of an integration point ignores the slash", () => + /* + * `title` here is HTML (inside foreignObject's HTML context), not a + * foreign integration point, so its slash must not be acknowledged. + */ + runTest("<svg><foreignObject><title/>x</foreignObject></svg>")); + it("HTML image alias", () => runTest("<image></image>")); it("SVG image is not aliased", () => runTest("<svg><image></image></svg>")); diff --git a/src/__snapshots__/Parser.events.spec.ts.snap b/src/__snapshots__/Parser.events.spec.ts.snap index 9c33556a2..30d563f9e 100644 --- a/src/__snapshots__/Parser.events.spec.ts.snap +++ b/src/__snapshots__/Parser.events.spec.ts.snap @@ -2767,6 +2767,167 @@ exports[`Events > Scripts ending with < 1`] = ` ] `; +exports[`Events > Self-closing HTML descendant of an integration point ignores the slash 1`] = ` +[ + { + "$event": "opentagname", + "data": [ + "svg", + ], + "endIndex": 4, + "startIndex": 0, + }, + { + "$event": "opentag", + "data": [ + "svg", + {}, + false, + ], + "endIndex": 4, + "startIndex": 0, + }, + { + "$event": "opentagname", + "data": [ + "foreignObject", + ], + "endIndex": 19, + "startIndex": 5, + }, + { + "$event": "opentag", + "data": [ + "foreignObject", + {}, + false, + ], + "endIndex": 19, + "startIndex": 5, + }, + { + "$event": "opentagname", + "data": [ + "title", + ], + "endIndex": 26, + "startIndex": 20, + }, + { + "$event": "opentag", + "data": [ + "title", + {}, + false, + ], + "endIndex": 27, + "startIndex": 20, + }, + { + "$event": "text", + "data": [ + "x</foreignObject></svg>", + ], + "endIndex": 50, + "startIndex": 28, + }, + { + "$event": "closetag", + "data": [ + "title", + true, + ], + "endIndex": 51, + "startIndex": 51, + }, + { + "$event": "closetag", + "data": [ + "foreignObject", + true, + ], + "endIndex": 51, + "startIndex": 51, + }, + { + "$event": "closetag", + "data": [ + "svg", + true, + ], + "endIndex": 51, + "startIndex": 51, + }, +] +`; + +exports[`Events > Self-closing MathML annotation-xml acknowledges the slash 1`] = ` +[ + { + "$event": "opentagname", + "data": [ + "math", + ], + "endIndex": 5, + "startIndex": 0, + }, + { + "$event": "opentag", + "data": [ + "math", + {}, + false, + ], + "endIndex": 5, + "startIndex": 0, + }, + { + "$event": "opentagname", + "data": [ + "annotation-xml", + ], + "endIndex": 21, + "startIndex": 6, + }, + { + "$event": "opentag", + "data": [ + "annotation-xml", + {}, + false, + ], + "endIndex": 22, + "startIndex": 6, + }, + { + "$event": "closetag", + "data": [ + "annotation-xml", + true, + ], + "endIndex": 22, + "startIndex": 6, + }, + { + "$event": "text", + "data": [ + "x", + ], + "endIndex": 23, + "startIndex": 23, + }, + { + "$event": "closetag", + "data": [ + "math", + false, + ], + "endIndex": 30, + "startIndex": 24, + }, +] +`; + exports[`Events > Self-closing MathML integration point acknowledges the slash 1`] = ` [ { @@ -2834,6 +2995,73 @@ exports[`Events > Self-closing MathML integration point acknowledges the slash 1 ] `; +exports[`Events > Self-closing SVG desc acknowledges the slash 1`] = ` +[ + { + "$event": "opentagname", + "data": [ + "svg", + ], + "endIndex": 4, + "startIndex": 0, + }, + { + "$event": "opentag", + "data": [ + "svg", + {}, + false, + ], + "endIndex": 4, + "startIndex": 0, + }, + { + "$event": "opentagname", + "data": [ + "desc", + ], + "endIndex": 10, + "startIndex": 5, + }, + { + "$event": "opentag", + "data": [ + "desc", + {}, + false, + ], + "endIndex": 11, + "startIndex": 5, + }, + { + "$event": "closetag", + "data": [ + "desc", + true, + ], + "endIndex": 11, + "startIndex": 5, + }, + { + "$event": "text", + "data": [ + "x", + ], + "endIndex": 12, + "startIndex": 12, + }, + { + "$event": "closetag", + "data": [ + "svg", + false, + ], + "endIndex": 18, + "startIndex": 13, + }, +] +`; + exports[`Events > Self-closing SVG integration point acknowledges the slash 1`] = ` [ {