Skip to content

Commit

Permalink
Merge pull request #4364 from preactjs/feat/mathml
Browse files Browse the repository at this point in the history
feat: Support MathML namespace
  • Loading branch information
rschristian committed May 2, 2024
2 parents f7e9bcb + 12b71cf commit 4ab4b99
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 33 deletions.
2 changes: 1 addition & 1 deletion src/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ function renderComponent(component) {
newVNode,
oldVNode,
component._globalContext,
component._parentDom.ownerSVGElement !== undefined,
component._parentDom.namespaceURI,
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 {string} 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
41 changes: 23 additions & 18 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 {string} 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 {string} 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,11 @@ 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 = 'http://www.w3.org/2000/svg';
else if (nodeType === 'math')
namespace = 'http://www.w3.org/1998/Math/MathML';
else if (!namespace) namespace = 'http://www.w3.org/1999/xhtml';

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

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

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

Expand All @@ -470,7 +473,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 +499,9 @@ function diffElementNodes(
newVNode,
oldVNode,
globalContext,
isSvg && nodeType !== 'foreignObject',
nodeType === 'foreignObject'
? 'http://www.w3.org/1999/xhtml'
: namespace,
excessDomChildren,
commitQueue,
excessDomChildren
Expand Down Expand Up @@ -530,12 +535,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 {string} 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 == 'http://www.w3.org/2000/svg') {
// Normalize incorrect prop usage for SVG:
// - xlink:href / xlinkHref --> href (xlink:href was removed from SVG and isn't needed)
// - className --> class
Expand Down
4 changes: 2 additions & 2 deletions src/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,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
2 changes: 1 addition & 1 deletion src/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function render(vnode, parentDom, replaceNode) {
vnode,
oldVNode || EMPTY_OBJ,
EMPTY_OBJ,
parentDom.ownerSVGElement !== undefined,
parentDom.namespaceURI,
!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

0 comments on commit 4ab4b99

Please sign in to comment.