Skip to content
Merged
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
21 changes: 15 additions & 6 deletions src/Components/Web.JS/src/Rendering/BrowserRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@

import { RenderBatch, ArrayBuilderSegment, RenderTreeEdit, RenderTreeFrame, EditType, FrameType, ArrayValues } from './RenderBatch/RenderBatch';
import { EventDelegator } from './Events/EventDelegator';
import { LogicalElement, PermutationListEntry, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement, permuteLogicalChildren, getClosestDomElement, emptyLogicalElement, getLogicalChildrenArray, depthFirstNodeTreeTraversal } from './LogicalElements';
import { LogicalElement, PermutationListEntry, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement, isMathMLElement, permuteLogicalChildren, getClosestDomElement, emptyLogicalElement, getLogicalChildrenArray, depthFirstNodeTreeTraversal } from './LogicalElements';
import { applyCaptureIdToElement } from './ElementReferenceCapture';
import { attachToEventDelegator as attachNavigationManagerToEventDelegator } from '../Services/NavigationManager';
import { applyAnyDeferredValue, tryApplySpecialProperty } from './DomSpecialPropertyUtil';
const sharedTemplateElemForParsing = document.createElement('template');
const sharedSvgElemForParsing = document.createElementNS('http://www.w3.org/2000/svg', 'g');
const sharedMathMLElemForParsing = document.createElementNS('http://www.w3.org/1998/Math/MathML', 'mrow');
const elementsToClearOnRootComponentRender = new Set<LogicalElement>();
const internalAttributeNamePrefix = '__internal_';
const eventPreventDefaultAttributeNamePrefix = 'preventDefault_';
Expand Down Expand Up @@ -266,9 +267,14 @@ export class BrowserRenderer {
const frameReader = batch.frameReader;
const tagName = frameReader.elementName(frame)!;

const newDomElementRaw = (tagName === 'svg' || isSvgElement(parent)) ?
document.createElementNS('http://www.w3.org/2000/svg', tagName) :
document.createElement(tagName);
let newDomElementRaw: Element;
if (tagName === 'svg' || isSvgElement(parent)) {
newDomElementRaw = document.createElementNS('http://www.w3.org/2000/svg', tagName);
} else if (tagName === 'math' || isMathMLElement(parent)) {
newDomElementRaw = document.createElementNS('http://www.w3.org/1998/Math/MathML', tagName);
} else {
newDomElementRaw = document.createElement(tagName);
}
const newElement = toLogicalElement(newDomElementRaw);

let inserted = false;
Expand Down Expand Up @@ -316,7 +322,7 @@ export class BrowserRenderer {
const markupContainer = createAndInsertLogicalContainer(parent, childIndex);

const markupContent = batch.frameReader.markupContent(markupFrame);
const parsedMarkup = parseMarkup(markupContent, isSvgElement(parent));
const parsedMarkup = parseMarkup(markupContent, isSvgElement(parent), isMathMLElement(parent));
let logicalSiblingIndex = 0;
while (parsedMarkup.firstChild) {
insertLogicalChild(parsedMarkup.firstChild, markupContainer, logicalSiblingIndex++);
Expand Down Expand Up @@ -410,10 +416,13 @@ export interface ComponentDescriptor {
end: Node;
}

function parseMarkup(markup: string, isSvg: boolean) {
function parseMarkup(markup: string, isSvg: boolean, isMathML: boolean) {
if (isSvg) {
sharedSvgElemForParsing.innerHTML = markup || ' ';
return sharedSvgElemForParsing;
} else if (isMathML) {
sharedMathMLElemForParsing.innerHTML = markup || ' ';
return sharedMathMLElemForParsing;
} else {
sharedTemplateElemForParsing.innerHTML = markup || ' ';

Expand Down
8 changes: 8 additions & 0 deletions src/Components/Web.JS/src/Rendering/LogicalElements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,14 @@ export function isSvgElement(element: LogicalElement): boolean {
return closestElement.namespaceURI === 'http://www.w3.org/2000/svg' && closestElement['tagName'] !== 'foreignObject';
}

// MathML elements need to be created with the MathML namespace to render correctly.
// Similar to SVG, MathML has its own namespace (http://www.w3.org/1998/Math/MathML)
// and elements created without this namespace will not render properly in browsers.
export function isMathMLElement(element: LogicalElement): boolean {
const closestElement = getClosestDomElement(element) as any;
return closestElement.namespaceURI === 'http://www.w3.org/1998/Math/MathML';
}

export function getLogicalChildrenArray(element: LogicalElement): LogicalElement[] {
return element[logicalChildrenPropname] as LogicalElement[];
}
Expand Down
117 changes: 117 additions & 0 deletions src/Components/test/E2ETest/Tests/MathMLTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using BasicTestApp;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using Xunit.Abstractions;

namespace Microsoft.AspNetCore.Components.E2ETest.Tests;

public class MathMLTest : ServerTestBase<ToggleExecutionModeServerFixture<Program>>
{
public MathMLTest(
BrowserFixture browserFixture,
ToggleExecutionModeServerFixture<Program> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
}

protected override void InitializeAsyncCore()
{
Navigate(ServerPathBase);
}

[Fact]
public void CanRenderMathMLWithCorrectNamespace()
{
var appElement = Browser.MountTestComponent<MathMLComponent>();

var mathElement = appElement.FindElement(By.Id("mathml-with-callback"));
Assert.NotNull(mathElement);

// Verify the math element has the correct MathML namespace
var mathMrowElement = mathElement.FindElement(By.XPath(".//*[local-name()='mrow' and namespace-uri()='http://www.w3.org/1998/Math/MathML']"));
Assert.NotNull(mathMrowElement);

// Verify child elements also have the correct namespace
var mathMnElement = mathElement.FindElement(By.XPath(".//*[local-name()='mn' and namespace-uri()='http://www.w3.org/1998/Math/MathML']"));
Assert.NotNull(mathMnElement);
Assert.Equal("10", mathMnElement.Text);

// Click button to update and verify the value changes while maintaining correct namespace
appElement.FindElement(By.Id("increment-btn")).Click();
Browser.Equal("11", () => mathMnElement.Text);
}

[Fact]
public void CanRenderStaticMathMLWithCorrectNamespace()
{
var appElement = Browser.MountTestComponent<MathMLComponent>();

var mathElement = appElement.FindElement(By.Id("mathml-static"));
Assert.NotNull(mathElement);

// Verify msup elements have the correct namespace
var msupElements = mathElement.FindElements(By.XPath(".//*[local-name()='msup' and namespace-uri()='http://www.w3.org/1998/Math/MathML']"));
Assert.Equal(3, msupElements.Count);
}

[Fact]
public void CanRenderMathMLChildComponentWithCorrectNamespace()
{
var appElement = Browser.MountTestComponent<MathMLComponent>();

var mathElement = appElement.FindElement(By.Id("mathml-with-child-component"));
Assert.NotNull(mathElement);

// The child component should render mrow with correct namespace
var mathMrowElement = mathElement.FindElement(By.XPath(".//*[local-name()='mrow' and namespace-uri()='http://www.w3.org/1998/Math/MathML']"));
Assert.NotNull(mathMrowElement);

// Verify mi element from child component
var mathMiElement = mathElement.FindElement(By.XPath(".//*[local-name()='mi' and namespace-uri()='http://www.w3.org/1998/Math/MathML']"));
Assert.NotNull(mathMiElement);
Assert.Equal("z", mathMiElement.Text);
}

[Fact]
public void CanRenderConditionalMathMLWithCorrectNamespace()
{
var appElement = Browser.MountTestComponent<MathMLComponent>();

// Initially the conditional MathML should not be present
var conditionalMath = appElement.FindElements(By.Id("mathml-conditional"));
Assert.Empty(conditionalMath);

// Click toggle to show the conditional MathML
appElement.FindElement(By.Id("toggle-btn")).Click();

// Now the MathML should be present with correct namespace
Browser.Exists(By.Id("mathml-conditional"));
var mathElement = appElement.FindElement(By.Id("mathml-conditional"));

var mathMrowElement = mathElement.FindElement(By.XPath(".//*[local-name()='mrow' and namespace-uri()='http://www.w3.org/1998/Math/MathML']"));
Assert.NotNull(mathMrowElement);
}

[Fact]
public void CanRenderComplexMathMLWithCorrectNamespace()
{
var appElement = Browser.MountTestComponent<MathMLComponent>();

var mathElement = appElement.FindElement(By.Id("mathml-complex"));
Assert.NotNull(mathElement);

// Verify mfrac element has the correct namespace
var mfracElement = mathElement.FindElement(By.XPath(".//*[local-name()='mfrac' and namespace-uri()='http://www.w3.org/1998/Math/MathML']"));
Assert.NotNull(mfracElement);

// Verify msqrt element has the correct namespace
var msqrtElement = mathElement.FindElement(By.XPath(".//*[local-name()='msqrt' and namespace-uri()='http://www.w3.org/1998/Math/MathML']"));
Assert.NotNull(msqrtElement);
}
}
1 change: 1 addition & 0 deletions src/Components/test/testassets/BasicTestApp/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
<option value="BasicTestApp.StringComparisonComponent">StringComparison</option>
<option value="BasicTestApp.SvgComponent">SVG</option>
<option value="BasicTestApp.SvgFocusComponent">SVG Focus component</option>
<option value="BasicTestApp.MathMLComponent">MathML</option>
<option value="BasicTestApp.TextOnlyComponent">Plain text</option>
<option value="BasicTestApp.ToggleEventComponent">Toggle Event</option>
<option value="BasicTestApp.DialogEventsComponent">Dialog Events</option>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
@* Test component for MathML rendering with correct namespace *@

<h3>MathML with Interactive Content</h3>
<math xmlns="http://www.w3.org/1998/Math/MathML" id="mathml-with-callback">
<mrow>
<mi>x</mi>
<mo>=</mo>
<mn id="mathml-value">@value</mn>
</mrow>
</math>

<button id="increment-btn" @onclick=@(() => value++)>Increment</button>

<h3>Static MathML</h3>
<math xmlns="http://www.w3.org/1998/Math/MathML" id="mathml-static">
<mrow>
<msup>
<mi>a</mi>
<mn>2</mn>
</msup>
<mo>+</mo>
<msup>
<mi>b</mi>
<mn>2</mn>
</msup>
<mo>=</mo>
<msup>
<mi>c</mi>
<mn>2</mn>
</msup>
</mrow>
</math>

<h3>MathML with Child Component</h3>
<math xmlns="http://www.w3.org/1998/Math/MathML" id="mathml-with-child-component">
<MathMLRowComponent />
</math>

<h3>Conditionally Rendered MathML</h3>
<button id="toggle-btn" @onclick=@(() => showConditional = !showConditional)>Toggle</button>
@if (showConditional)
{
<math xmlns="http://www.w3.org/1998/Math/MathML" id="mathml-conditional">
<mrow>
<mi>y</mi>
<mo>=</mo>
<mn>42</mn>
</mrow>
</math>
}

<h3>Complex MathML (Quadratic Formula)</h3>
<math xmlns="http://www.w3.org/1998/Math/MathML" id="mathml-complex">
<mrow>
<mi>x</mi>
<mo>=</mo>
<mfrac>
<mrow>
<mo>-</mo>
<mi>b</mi>
<mo>±</mo>
<msqrt>
<mrow>
<msup><mi>b</mi><mn>2</mn></msup>
<mo>-</mo>
<mn>4</mn>
<mi>a</mi>
<mi>c</mi>
</mrow>
</msqrt>
</mrow>
<mrow>
<mn>2</mn>
<mi>a</mi>
</mrow>
</mfrac>
</mrow>
</math>

@code {
int value = 10;
bool showConditional = false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@* Child component for MathML that renders a simple row *@

<mrow>
<mi>z</mi>
<mo>=</mo>
<mn>100</mn>
</mrow>
Loading