Skip to content

Refactor SVG/MathML namespace handling#55

Merged
adebola-io merged 2 commits into
mainfrom
fix-svg-namespaces
May 10, 2026
Merged

Refactor SVG/MathML namespace handling#55
adebola-io merged 2 commits into
mainfrom
fix-svg-namespaces

Conversation

@adebola-io
Copy link
Copy Markdown
Collaborator

Introduce a scope for managing XML namespaces. This allows for proper handling of SVG and MathML elements within nested structures, including reactive branches and teleported content.

This change also improves the createContainer logic by inheriting namespaces from parent scopes and setting them explicitly when needed, rather than relying on string manipulation or re-serialization.

Introduce a scope for managing XML namespaces. This allows for proper
handling of SVG and MathML elements within nested structures, including
reactive branches and teleported content.

This change also improves the `createContainer` logic by inheriting
namespaces from parent scopes and setting them explicitly when needed,
rather than relying on string manipulation or re-serialization.
@qodo-code-review
Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Refactor SVG/MathML namespace handling with scope-based inheritance

✨ Enhancement 🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Introduce namespace scope management for SVG/MathML elements
• Replace string serialization workaround with scope-based namespace inheritance
• Wrap child components with NamespaceScope.Provider to preserve namespaces
• Handle foreignObject namespace switching back to HTML
• Add comprehensive test coverage for namespace preservation
Diagram
flowchart LR
  A["createContainer"] -->|"reads inherited namespace"| B["NamespaceScope.Provider"]
  B -->|"wraps children"| C["SVG/MathML elements"]
  C -->|"preserve namespace"| D["Nested components"]
  E["foreignObject"] -->|"switches to HTML"| F["HTML descendants"]
  G["Old approach: innerHTML serialization"] -.->|"removed"| H["New approach: scope inheritance"]
Loading

Grey Divider

File Changes

1. packages/retend-web/source/dom-renderer.js ✨ Enhancement +65/-59

Replace innerHTML workaround with scope-based namespace management

• Import createScope and useScopeContext from retend library
• Create NamespaceScope to manage XML namespace context across component boundaries
• Define namespace constants for HTML, SVG, and MathML
• Remove old innerHTML serialization workaround in appendNode method
• Refactor createContainer to use scope-based namespace inheritance instead of string manipulation
• Wrap child components with NamespaceScope.Provider to propagate namespace context
• Handle foreignObject special case to switch descendants back to HTML namespace

packages/retend-web/source/dom-renderer.js


2. tests/attributes.spec.tsx 🧪 Tests +196/-2

Add comprehensive namespace preservation test coverage

• Import additional testing utilities and components (If, Switch, Teleport, etc.)
• Define namespace URI constants for test assertions
• Add 7 new test cases covering SVG namespace preservation through component boundaries
• Test SVG interactivity preservation with event handlers
• Test foreignObject namespace switching behavior
• Test MathML namespace preservation through components
• Test namespace preservation through reactive If/Switch branches and Teleport
• Test namespace preservation for Unique components after moves

tests/attributes.spec.tsx


3. tests/hydration/hydration.spec.tsx 🧪 Tests +23/-0

Add hydration test for SVG namespace preservation

• Add new test case for SVG namespace preservation during hydration
• Test that SVG elements maintain correct namespace through hydrated If blocks
• Verify namespace preservation when switching between true/false branches

tests/hydration/hydration.spec.tsx


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown
Contributor

qodo-code-review Bot commented May 8, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0)

Grey Divider


Remediation recommended

1. SVG child namespace regression 🐞 Bug ≡ Correctness
Description
SVG/MathML child elements rendered without an active NamespaceScope (e.g., rendering a standalone
<circle/> and appending into an existing <svg>/<math>) will be created in the HTML namespace and are
no longer corrected when appended, so they won’t behave as real SVG/MathML elements.
Code

packages/retend-web/source/dom-renderer.js[R357-418]

+    let inheritedNamespace = HTML_NAMESPACE;
+    try {
+      inheritedNamespace = useScopeContext(NamespaceScope);
+    } catch {}

-    const defaultNamespace = props?.xmlns ?? 'http://www.w3.org/1999/xhtml';
+    const defaultNamespace = props?.xmlns ?? inheritedNamespace;
    let ns;
-    if (tagname === 'svg') {
-      ns = 'http://www.w3.org/2000/svg';
-    } else if (tagname === 'math') {
-      ns = 'http://www.w3.org/1998/Math/MathML';
-    } else {
-      ns = defaultNamespace;
+    if (tagname === 'svg') ns = SVG_NAMESPACE;
+    else if (tagname === 'math') ns = MATH_NAMESPACE;
+    else ns = defaultNamespace;
+
+    if (
+      props &&
+      props.xmlns === undefined &&
+      (tagname === 'svg' || tagname === 'math')
+    ) {
+      props.xmlns = ns;
+    }
+
+    const hydration = this.#isHydrationModeEnabled
+      ? this.#getHydrationState()
+      : null;
+    const isDynamic =
+      hydration && containerIsDynamic(tagname, props, isReactiveChild);
+
+    if (props && tagname !== 'retend-teleport' && 'children' in props) {
+      const childNamespace = tagname === 'foreignObject' ? HTML_NAMESPACE : ns;
+      const children = props.children;
+      props.children = () =>
+        NamespaceScope.Provider({
+          value: childNamespace,
+          children: () => children,
+          h: false,
+        });
+    }
+
+    if (hydration) {
+      if (isDynamic) {
+        const branchCursor = hydration.cursor;
+        const activeBranch = getState();
+        const index = `${activeBranch.node.id}.${branchCursor}`;
+        hydration.cursor += 1;
+
+        const staticNode = this.#table.get(index);
+        const hydrationNode =
+          staticNode && !this.#hydratedNodes.has(staticNode)
+            ? staticNode
+            : null;
+        if (hydrationNode) {
+          this.#hydratedNodes.add(hydrationNode);
+          const hydrationTask = Promise.resolve().then(() =>
+            this.#hydrateNode(hydrationNode, props)
+          );
+          this.#trackHydrationTask(activeBranch, hydrationTask);
+          return hydrationNode;
+        }
+      }
+      // @ts-expect-error: The types are different in hydration mode.
+      return new Skip(tagname);
    }

    /** @type {JsxElement} */ // @ts-expect-error
Evidence
DOMRenderer.createContainer defaults to HTML_NAMESPACE when NamespaceScope is missing and only
special-cases the root tags 'svg'/'math'; all other tags inherit the default and are created with
createElementNS using that namespace. DOMRenderer.append no longer contains any logic to correct a
child node’s namespace based on the parent’s namespace. The retend docs/examples show that
renderer.render(...) output is intended to be appended into existing DOM nodes, which makes this
standalone-render-then-append pattern plausible.

packages/retend-web/source/dom-renderer.js[289-320]
packages/retend-web/source/dom-renderer.js[356-421]
packages/retend/source/library/block.js[44-57]
packages/retend/source/library/if.js[29-54]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The SVG/MathML namespace handling now relies on NamespaceScope propagation at creation time, but there is no longer any fallback when a node is rendered outside that scope and later appended into an existing `<svg>`/`<math>` element. In that case the node remains HTML-namespaced and won’t behave as a real SVG/MathML element.

### Issue Context
Previously there was an append-time bailout to correct this; it was removed in this refactor. The new `createContainer()` logic only ensures correctness when the namespace scope is present during creation.

### Fix Focus Areas
- packages/retend-web/source/dom-renderer.js[289-320]
- packages/retend-web/source/dom-renderer.js[356-421]

### Implementation notes
- In `append(parent, child)`, detect cases where `parentNode.namespaceURI` is SVG/MathML but `childNode` is an Element in the HTML namespace.
- Replace the child with a namespace-correct clone created via `document.createElementNS(parentNode.namespaceURI, childNode.localName)` and migrate:
 - attributes
 - child nodes
 - retend internal listener maps (`__eventListenerList`, etc.) by reattaching listeners to the new element
- Avoid outerHTML re-serialization so event handlers/interactivity aren’t lost.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

2. Scope errors silently swallowed 🐞 Bug ◔ Observability
Description
createContainer suppresses all exceptions from useScopeContext(NamespaceScope), which can hide
unexpected runtime errors and silently fall back to the HTML namespace.
Code

packages/retend-web/source/dom-renderer.js[R357-360]

+    let inheritedNamespace = HTML_NAMESPACE;
+    try {
+      inheritedNamespace = useScopeContext(NamespaceScope);
+    } catch {}
Evidence
useScopeContext is expected to throw MissingScopeError when no provider exists; catching all errors
means non-missing-scope failures would be masked and mis-rendering could occur without surfacing the
real cause.

packages/retend-web/source/dom-renderer.js[356-361]
packages/retend/source/library/scope.js[289-329]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`createContainer()` currently catches all exceptions from `useScopeContext(NamespaceScope)` and falls back to the HTML namespace. This can mask unexpected issues (state corruption, programmer error) that should surface.

### Issue Context
`useScopeContext()` throws `MissingScopeError` when the scope isn’t provided; that case is expected at the root. Other exceptions should not be silently swallowed.

### Fix Focus Areas
- packages/retend-web/source/dom-renderer.js[356-361]
- packages/retend/source/library/scope.js[289-329]

### Implementation notes
- Change `catch {}` to `catch (e) { if (e instanceof MissingScopeError) { /* fallback */ } else { throw e } }`.
- If importing `MissingScopeError` isn’t desirable, alternatively check `e?.name === 'MissingScopeError'` or expose a helper from `retend` for this check.
- (Optional) In DEV, log a warning when falling back due to missing scope to aid debugging.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@cloudflare-workers-and-pages
Copy link
Copy Markdown
Contributor

cloudflare-workers-and-pages Bot commented May 8, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
retend 69529cb Commit Preview URL

Branch Preview URL
May 10 2026, 03:37 PM

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bc517667c6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +362 to +366
const defaultNamespace = props?.xmlns ?? inheritedNamespace;
let ns;
if (tagname === 'svg') {
ns = 'http://www.w3.org/2000/svg';
} else if (tagname === 'math') {
ns = 'http://www.w3.org/1998/Math/MathML';
} else {
ns = defaultNamespace;
if (tagname === 'svg') ns = SVG_NAMESPACE;
else if (tagname === 'math') ns = MATH_NAMESPACE;
else ns = defaultNamespace;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep internal teleport container in HTML namespace

This fallback makes every non-svg/math tag inherit the parent namespace, so a <Teleport> rendered inside <svg> now creates retend-teleport as an SVGElement instead of an HTMLElement. That breaks downstream logic that expects teleported containers to be HTML nodes (for example, copyComponentScreenshot only accepts __retendTeleportedContainer when instanceof HTMLElement), so teleported SVG components can no longer be captured correctly. Internal wrapper tags like retend-teleport should stay in HTML_NAMESPACE even when their source scope is SVG/MathML.

Useful? React with 👍 / 👎.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 10, 2026

Open in StackBlitz

retend

npm i https://pkg.pr.new/resuite/retend@55

retend-oxlint-plugin

npm i https://pkg.pr.new/resuite/retend/retend-oxlint-plugin@55

retend-server

npm i https://pkg.pr.new/resuite/retend/retend-server@55

retend-start

npm i https://pkg.pr.new/resuite/retend/retend-start@55

retend-utils

npm i https://pkg.pr.new/resuite/retend/retend-utils@55

retend-web

npm i https://pkg.pr.new/resuite/retend/retend-web@55

retend-web-devtools

npm i https://pkg.pr.new/resuite/retend/retend-web-devtools@55

commit: 69529cb

@adebola-io adebola-io merged commit 31914dd into main May 10, 2026
4 checks passed
@adebola-io adebola-io deleted the fix-svg-namespaces branch May 10, 2026 15:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant