Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/Parser.events.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,28 @@ 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 SVG desc acknowledges the slash", () =>
runTest("<svg><desc/>x</svg>"));

it("Self-closing MathML integration point acknowledges the slash", () =>
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
runTest("<math><mi/>x</math>"));

Comment thread
coderabbitai[bot] marked this conversation as resolved.
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>"));
Expand Down
21 changes: 20 additions & 1 deletion src/Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

@cubic-dev-ai cubic-dev-ai Bot Aug 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The foreignContext[1] index is a magic number that depends on an implicit invariant: every integration element in HTML mode pushes ForeignContext.None onto the stack in emitOpenTag, so after the current tag is opened index 1 is always the enclosing context. This coupling is easy to miss, and if a future change stops pushing a None entry for an integration element, foreignContext[1] becomes undefined, and undefined !== ForeignContext.None is true, silently misclassifying the tag as foreign (self-closing slash honored for a document-level <title/>, <mi/>, etc.). Make the intent explicit and robust by checking the enclosing entry via length, e.g. this.foreignContext.length > 1 && this.foreignContext[1] !== ForeignContext.None, and add the invariant to the doc comment.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Parser.ts, line 356:

<comment>The `foreignContext[1]` index is a magic number that depends on an implicit invariant: every integration element in HTML mode pushes `ForeignContext.None` onto the stack in `emitOpenTag`, so after the current tag is opened index 1 is always the *enclosing* context. This coupling is easy to miss, and if a future change stops pushing a `None` entry for an integration element, `foreignContext[1]` becomes `undefined`, and `undefined !== ForeignContext.None` is `true`, silently misclassifying the tag as foreign (self-closing slash honored for a document-level `<title/>`, `<mi/>`, etc.). Make the intent explicit and robust by checking the enclosing entry via length, e.g. `this.foreignContext.length > 1 && this.foreignContext[1] !== ForeignContext.None`, and add the invariant to the doc comment.</comment>

<file context>
@@ -338,6 +338,25 @@ export class Parser implements Callbacks {
+            this.isInForeignContext() ||
+            (this.htmlMode &&
+                htmlIntegrationElements.has(this.tagname) &&
+                this.foreignContext[1] !== ForeignContext.None)
+        );
+    }
</file context>
Suggested change
this.foreignContext[1] !== ForeignContext.None)
this.foreignContext.length > 1 &&
this.foreignContext[1] !== ForeignContext.None)
Fix with cubic

);
}

/**
* Checks if the current tag is a void element. Override this if you want
* to specify your own additional void elements.
Expand Down Expand Up @@ -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
Expand Down
Loading