From 622f1d78f93fa8ebd748ea34c7e1e449999522be Mon Sep 17 00:00:00 2001 From: Alex Kanunnikov Date: Mon, 6 Jan 2025 15:15:12 +0300 Subject: [PATCH] Feature: Basic mathml support --- .../integration-tests/test/math-test.ts | 82 +++++++++++++++++++ .../@glimmer/runtime/lib/dom/operations.ts | 19 ++++- 2 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 packages/@glimmer-workspace/integration-tests/test/math-test.ts diff --git a/packages/@glimmer-workspace/integration-tests/test/math-test.ts b/packages/@glimmer-workspace/integration-tests/test/math-test.ts new file mode 100644 index 0000000000..4cd524eea4 --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/math-test.ts @@ -0,0 +1,82 @@ +import type { Namespace } from '@glimmer/interfaces'; +import { NS_HTML, NS_MATHML, NS_SVG } from '@glimmer/constants'; + +import { defineComponent, jitSuite, RenderTest, test } from '..'; + +class MathElementTest extends RenderTest { + static suiteName = ''; + query(selector: string) { + let el = (s: string) => (this.element as unknown as HTMLElement).querySelector(s); + return el(selector) as Element; + } + assertNamespace(selector: string, ns: Namespace) { + this.assert.strictEqual( + this.query(selector).namespaceURI, + ns, + `Expecting "${ns}" namespace for tag "${selector}"` + ); + } + + @test + ' element can render'() { + const Bar = defineComponent({}, 'x'); + + this.renderComponent(Bar); + + this.assertNamespace('math', NS_MATHML); + this.assertNamespace('msqrt', NS_MATHML); + this.assertNamespace('mi', NS_MATHML); + } + + @test + 'HTML and element can render together'() { + const Bar = defineComponent( + {}, + '

Math inside:

x
' + ); + + this.renderComponent(Bar); + + this.assertNamespace('div', NS_HTML); + this.assertNamespace('p', NS_HTML); + this.assertNamespace('math', NS_MATHML); + this.assertNamespace('msqrt', NS_MATHML); + this.assertNamespace('mi', NS_MATHML); + } + + @test + 'SVG and element can render together'() { + const Bar = defineComponent( + {}, + 'x' + ); + + this.renderComponent(Bar); + + this.assertNamespace('svg', NS_SVG); + this.assertNamespace('circle', NS_SVG); + this.assertNamespace('math', NS_MATHML); + this.assertNamespace('msqrt', NS_MATHML); + this.assertNamespace('mi', NS_MATHML); + } + + @test + 'HTML, SVG, and element can render together'() { + const Bar = defineComponent( + {}, + '

Math and SVG inside:

x
' + ); + + this.renderComponent(Bar); + + this.assertNamespace('div', NS_HTML); + this.assertNamespace('p', NS_HTML); + this.assertNamespace('svg', NS_SVG); + this.assertNamespace('circle', NS_SVG); + this.assertNamespace('math', NS_MATHML); + this.assertNamespace('msqrt', NS_MATHML); + this.assertNamespace('mi', NS_MATHML); + } +} + +jitSuite(MathElementTest); diff --git a/packages/@glimmer/runtime/lib/dom/operations.ts b/packages/@glimmer/runtime/lib/dom/operations.ts index e1bb0886a2..0be421f53c 100644 --- a/packages/@glimmer/runtime/lib/dom/operations.ts +++ b/packages/@glimmer/runtime/lib/dom/operations.ts @@ -1,6 +1,7 @@ import type { Bounds, Dict, + Namespace, Nullable, SimpleComment, SimpleDocument, @@ -8,7 +9,7 @@ import type { SimpleNode, SimpleText, } from '@glimmer/interfaces'; -import { INSERT_BEFORE_BEGIN, INSERT_BEFORE_END, NS_SVG } from '@glimmer/constants'; +import { INSERT_BEFORE_BEGIN, INSERT_BEFORE_END, NS_MATHML, NS_SVG } from '@glimmer/constants'; import { expect } from '@glimmer/debug-util'; import { ConcreteBounds } from '../bounds'; @@ -39,25 +40,35 @@ export class DOMOperations { } createElement(tag: string, context?: SimpleElement): SimpleElement { - let isElementInSVGNamespace: boolean, isHTMLIntegrationPoint: boolean; + let isElementInSVGNamespace: boolean, + isHTMLIntegrationPoint: boolean, + isElementInMathMlNamespace: boolean, + ns: Namespace; if (context) { isElementInSVGNamespace = context.namespaceURI === NS_SVG || tag === 'svg'; + isElementInMathMlNamespace = context.namespaceURI === NS_MATHML || tag === 'math'; isHTMLIntegrationPoint = !!(SVG_INTEGRATION_POINTS as Dict)[context.tagName]; } else { isElementInSVGNamespace = tag === 'svg'; + isElementInMathMlNamespace = tag === 'math'; isHTMLIntegrationPoint = false; } - if (isElementInSVGNamespace && !isHTMLIntegrationPoint) { + if ((isElementInMathMlNamespace || isElementInSVGNamespace) && !isHTMLIntegrationPoint) { // FIXME: This does not properly handle with color, face, or // size attributes, which is also disallowed by the spec. We should fix // this. if (BLACKLIST_TABLE[tag]) { throw new Error(`Cannot create a ${tag} inside an SVG context`); } + if (isElementInMathMlNamespace) { + ns = NS_MATHML; + } else { + ns = NS_SVG; + } - return this.document.createElementNS(NS_SVG, tag); + return this.document.createElementNS(ns, tag); } else { return this.document.createElement(tag); }