Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support MathML namespace #4364

Merged
merged 13 commits into from
May 2, 2024
Merged
7 changes: 6 additions & 1 deletion src/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,12 @@ function renderComponent(component) {
newVNode,
oldVNode,
component._globalContext,
component._parentDom.ownerSVGElement !== undefined,
component._parentDom.namespaceURI == 'http://www.w3.org/2000/svg'
rschristian marked this conversation as resolved.
Show resolved Hide resolved
? 2
: component._parentDom.namespaceURI ==
'http://www.w3.org/1998/Math/MathML'
? 3
: 1,
oldVNode._flags & MODE_HYDRATE ? [oldDom] : null,
commitQueue,
oldDom == null ? getDomSibling(oldVNode) : oldDom,
Expand Down
6 changes: 3 additions & 3 deletions src/diff/children.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { getDomSibling } from '../component';
* diff'ed against newParentVNode
* @param {object} globalContext The current context object - modified by
* getChildContext
* @param {boolean} isSvg Whether or not this DOM node is an SVG node
* @param {ElementNamespace} namespace Current namespace of the DOM node (HTML, SVG, or MathML)
* @param {Array<PreactElement>} excessDomChildren
* @param {Array<Component>} commitQueue List of components which have callbacks
* to invoke in commitRoot
Expand All @@ -32,7 +32,7 @@ export function diffChildren(
newParentVNode,
oldParentVNode,
globalContext,
isSvg,
namespace,
excessDomChildren,
commitQueue,
oldDom,
Expand Down Expand Up @@ -87,7 +87,7 @@ export function diffChildren(
childVNode,
oldVNode,
globalContext,
isSvg,
namespace,
excessDomChildren,
commitQueue,
oldDom,
Expand Down
38 changes: 22 additions & 16 deletions src/diff/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import options from '../options';
* @param {VNode} oldVNode The old virtual node
* @param {object} globalContext The current context object. Modified by
* getChildContext
* @param {boolean} isSvg Whether or not this element is an SVG node
* @param {ElementNamespace} namespace Current namespace of the DOM node (HTML, SVG, or MathML)
* @param {Array<PreactElement>} excessDomChildren
* @param {Array<Component>} commitQueue List of components which have callbacks
* to invoke in commitRoot
Expand All @@ -34,7 +34,7 @@ export function diff(
newVNode,
oldVNode,
globalContext,
isSvg,
namespace,
excessDomChildren,
commitQueue,
oldDom,
Expand Down Expand Up @@ -245,7 +245,7 @@ export function diff(
newVNode,
oldVNode,
globalContext,
isSvg,
namespace,
excessDomChildren,
commitQueue,
oldDom,
Expand Down Expand Up @@ -294,7 +294,7 @@ export function diff(
newVNode,
oldVNode,
globalContext,
isSvg,
namespace,
excessDomChildren,
commitQueue,
isHydrating,
Expand Down Expand Up @@ -341,7 +341,7 @@ export function commitRoot(commitQueue, root, refQueue) {
* @param {VNode} newVNode The new virtual node
* @param {VNode} oldVNode The old virtual node
* @param {object} globalContext The current context object
* @param {boolean} isSvg Whether or not this DOM node is an SVG node
* @param {ElementNamespace} namespace Current namespace of the DOM node (HTML, SVG, or MathML)
* @param {Array<PreactElement>} excessDomChildren
* @param {Array<Component>} commitQueue List of components which have callbacks
* to invoke in commitRoot
Expand All @@ -354,7 +354,7 @@ function diffElementNodes(
newVNode,
oldVNode,
globalContext,
isSvg,
namespace,
excessDomChildren,
commitQueue,
isHydrating,
Expand All @@ -375,8 +375,9 @@ function diffElementNodes(
let inputValue;
let checked;

// Tracks entering and exiting SVG namespace when descending through the tree.
if (nodeType === 'svg') isSvg = true;
// Tracks entering and exiting namespaces when descending through the tree.
if (nodeType === 'svg') namespace = 2;
else if (nodeType === 'math') namespace = 3;

if (excessDomChildren != null) {
for (i = 0; i < excessDomChildren.length; i++) {
Expand All @@ -402,10 +403,15 @@ function diffElementNodes(
return document.createTextNode(newProps);
}

if (isSvg) {
dom = document.createElementNS('http://www.w3.org/2000/svg', nodeType);
} else {
if (namespace == 1) {
dom = document.createElement(nodeType, newProps.is && newProps);
} else {
dom = document.createElementNS(
namespace == 2
? 'http://www.w3.org/2000/svg'
: 'http://www.w3.org/1998/Math/MathML',
nodeType
);
}

// we created a new parent, so none of the previously attached children can be reused:
Expand Down Expand Up @@ -449,7 +455,7 @@ function diffElementNodes(
) {
continue;
}
setProperty(dom, i, null, value, isSvg);
setProperty(dom, i, null, value, namespace);
}
}

Expand All @@ -470,7 +476,7 @@ function diffElementNodes(
(!isHydrating || typeof value == 'function') &&
oldProps[i] !== value
) {
setProperty(dom, i, value, oldProps[i], isSvg);
setProperty(dom, i, value, oldProps[i], namespace);
}
}

Expand All @@ -496,7 +502,7 @@ function diffElementNodes(
newVNode,
oldVNode,
globalContext,
isSvg && nodeType !== 'foreignObject',
nodeType === 'foreignObject' ? 1 : namespace,
excessDomChildren,
commitQueue,
excessDomChildren
Expand Down Expand Up @@ -530,12 +536,12 @@ function diffElementNodes(
// again, which triggers IE11 to re-evaluate the select value
(nodeType === 'option' && inputValue !== oldProps[i]))
) {
setProperty(dom, i, inputValue, oldProps[i], false);
setProperty(dom, i, inputValue, oldProps[i], namespace);
}

i = 'checked';
if (checked !== undefined && checked !== dom[i]) {
setProperty(dom, i, checked, oldProps[i], false);
setProperty(dom, i, checked, oldProps[i], namespace);
}
}
}
Expand Down
14 changes: 7 additions & 7 deletions src/diff/props.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ let eventClock = 0;
* @param {string} name The name of the property to set
* @param {*} value The value to set the property to
* @param {*} oldValue The old value the property had
* @param {boolean} isSvg Whether or not this DOM node is an SVG node or not
* @param {ElementNamespace} namespace Whether or not this DOM node is an SVG node or not
*/
export function setProperty(dom, name, value, oldValue, isSvg) {
export function setProperty(dom, name, value, oldValue, namespace) {
let useCapture;

o: if (name === 'style') {
Expand Down Expand Up @@ -83,10 +83,10 @@ export function setProperty(dom, name, value, oldValue, isSvg) {
if (!oldValue) {
value._attached = eventClock;
dom.addEventListener(
name,
useCapture ? eventProxyCapture : eventProxy,
useCapture
);
name,
useCapture ? eventProxyCapture : eventProxy,
useCapture
);
} else {
value._attached = oldValue._attached;
}
Expand All @@ -98,7 +98,7 @@ export function setProperty(dom, name, value, oldValue, isSvg) {
);
}
} else {
if (isSvg) {
if (namespace == 2) {
// Normalize incorrect prop usage for SVG:
// - xlink:href / xlinkHref --> href (xlink:href was removed from SVG and isn't needed)
// - className --> class
Expand Down
10 changes: 8 additions & 2 deletions src/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ declare global {
useDebugvalue = 11
}

export enum ElementNamespace {
html = 1,
svg = 2,
mathml = 3
}

export interface DevSource {
fileName: string;
lineNumber: number;
Expand Down Expand Up @@ -83,8 +89,8 @@ declare global {
export type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>;

export interface PreactElement extends preact.ContainerNode {
// SVG detection
readonly ownerSVGElement?: SVGElement['ownerSVGElement'];
// Namespace detection
readonly namespaceURI?: string;
// Property used to update Text nodes
data?: CharacterData['data'];
// Property to set __dangerouslySetInnerHTML
Expand Down
6 changes: 5 additions & 1 deletion src/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ export function render(vnode, parentDom, replaceNode) {
vnode,
oldVNode || EMPTY_OBJ,
EMPTY_OBJ,
parentDom.ownerSVGElement !== undefined,
parentDom.namespaceURI == 'http://www.w3.org/2000/svg'
rschristian marked this conversation as resolved.
Show resolved Hide resolved
rschristian marked this conversation as resolved.
Show resolved Hide resolved
? 2
: parentDom.namespaceURI == 'http://www.w3.org/1998/Math/MathML'
? 3
: 1,
!isHydrating && replaceNode
? [replaceNode]
: oldVNode
Expand Down
95 changes: 95 additions & 0 deletions test/browser/mathml.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { createElement, Component, render } from 'preact';
import { setupRerender } from 'preact/test-utils';
import { setupScratch, teardown } from '../_util/helpers';

/** @jsx createElement */

describe('mathml', () => {
let scratch;

beforeEach(() => {
scratch = setupScratch();
});

afterEach(() => {
teardown(scratch);
});

it('should render with the correct namespace URI', () => {
render(<math />, scratch);

let namespace = scratch.querySelector('math').namespaceURI;

expect(namespace).to.equal('http://www.w3.org/1998/Math/MathML');
});

it('should render children with the correct namespace URI', () => {
render(
<math>
<mrow />
</math>,
scratch
);

let namespace = scratch.querySelector('mrow').namespaceURI;

expect(namespace).to.equal('http://www.w3.org/1998/Math/MathML');
});

it('should inherit correct namespace URI from parent', () => {
const math = document.createElementNS(
'http://www.w3.org/1998/Math/MathML',
'math'
);
scratch.appendChild(math);

render(<mrow />, scratch.firstChild);

let namespace = scratch.querySelector('mrow').namespaceURI;
expect(namespace).to.equal('http://www.w3.org/1998/Math/MathML');
});

it('should inherit correct namespace URI from parent upon updating', () => {
setupRerender();

const math = document.createElementNS(
'http://www.w3.org/1998/Math/MathML',
'math'
);
scratch.appendChild(math);

class App extends Component {
state = { show: true };
componentDidMount() {
// eslint-disable-next-line
this.setState({ show: false }, () => {
expect(scratch.querySelector('mo').namespaceURI).to.equal(
'http://www.w3.org/1998/Math/MathML'
);
});
}
render() {
return this.state.show ? <mi>1</mi> : <mo>2</mo>;
}
}

render(<App />, scratch.firstChild);
});

it('should transition from DOM to MathML and back', () => {
render(
<div>
<math>
<msup>
<mi>c</mi>
<mn>2</mn>
</msup>
</math>
</div>,
scratch
);

expect(scratch.firstChild).to.be.an('HTMLDivElement');
expect(scratch.firstChild.firstChild).to.be.an('MathMLElement');
});
});
51 changes: 50 additions & 1 deletion test/browser/svg.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createElement, render } from 'preact';
import { createElement, Component, render } from 'preact';
import { setupRerender } from 'preact/test-utils';
import { setupScratch, teardown, sortAttributes } from '../_util/helpers';

/** @jsx createElement */
Expand Down Expand Up @@ -133,6 +134,54 @@ describe('svg', () => {
expect(namespace).to.equal('http://www.w3.org/2000/svg');
});

it('should render children with the correct namespace URI', () => {
render(
<svg>
<text />
</svg>,
scratch
);

let namespace = scratch.querySelector('text').namespaceURI;

expect(namespace).to.equal('http://www.w3.org/2000/svg');
});

it('should inherit correct namespace URI from parent', () => {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
scratch.appendChild(svg);

render(<text />, scratch.firstChild);

let namespace = scratch.querySelector('text').namespaceURI;

expect(namespace).to.equal('http://www.w3.org/2000/svg');
});

it('should inherit correct namespace URI from parent upon updating', () => {
setupRerender();

const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
scratch.appendChild(svg);

class App extends Component {
state = { show: true };
componentDidMount() {
// eslint-disable-next-line
this.setState({ show: false }, () => {
expect(scratch.querySelector('circle').namespaceURI).to.equal(
'http://www.w3.org/2000/svg'
);
});
}
render() {
return this.state.show ? <text /> : <circle />;
}
}

render(<App />, scratch.firstChild);
});

it('should use attributes for className', () => {
const Demo = ({ c }) => (
<svg viewBox="0 0 360 360" {...(c ? { class: 'foo_' + c } : {})}>
Expand Down
Loading