diff --git a/packages/compiler-dom/src/parserOptions.ts b/packages/compiler-dom/src/parserOptions.ts index 2b5e9f01084..244251b9ba9 100644 --- a/packages/compiler-dom/src/parserOptions.ts +++ b/packages/compiler-dom/src/parserOptions.ts @@ -1,12 +1,12 @@ import { ParserOptions, NodeTypes, Namespaces } from '@vue/compiler-core' -import { isVoidTag, isHTMLTag, isSVGTag } from '@vue/shared' +import { isVoidTag, isHTMLTag, isSVGTag, isMathMLTag } from '@vue/shared' import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers' import { decodeHtmlBrowser } from './decodeHtmlBrowser' export const parserOptions: ParserOptions = { parseMode: 'html', isVoidTag, - isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag), + isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag) || isMathMLTag(tag), isPreTag: tag => tag === 'pre', decodeEntities: __BROWSER__ ? decodeHtmlBrowser : undefined, diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index 7c0a15b2e98..423c81d7441 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -16,7 +16,7 @@ import { ComponentPublicInstance } from './componentPublicInstance' import { Directive, validateDirectiveName } from './directives' -import { RootRenderFunction } from './renderer' +import { ElementNamespace, RootRenderFunction } from './renderer' import { InjectionKey } from './apiInject' import { warn } from './warning' import { createVNode, cloneVNode, VNode } from './vnode' @@ -47,7 +47,7 @@ export interface App<HostElement = any> { mount( rootContainer: HostElement | string, isHydrate?: boolean, - isSVG?: boolean + namespace?: boolean | ElementNamespace ): ComponentPublicInstance unmount(): void provide<T>(key: InjectionKey<T> | string, value: T): this @@ -297,7 +297,7 @@ export function createAppAPI<HostElement>( mount( rootContainer: HostElement, isHydrate?: boolean, - isSVG?: boolean + namespace?: boolean | ElementNamespace ): any { if (!isMounted) { // #5571 @@ -313,17 +313,29 @@ export function createAppAPI<HostElement>( // this will be set on the root instance on initial mount. vnode.appContext = context + if (namespace === true) { + namespace = 'svg' + } else if (namespace === false) { + namespace = undefined + } + // HMR root reload if (__DEV__) { context.reload = () => { - render(cloneVNode(vnode), rootContainer, isSVG) + // casting to ElementNamespace because TS doesn't guarantee type narrowing + // over function boundaries + render( + cloneVNode(vnode), + rootContainer, + namespace as ElementNamespace + ) } } if (isHydrate && hydrate) { hydrate(vnode as VNode<Node, Element>, rootContainer as any) } else { - render(vnode, rootContainer, isSVG) + render(vnode, rootContainer, namespace) } isMounted = true app._container = rootContainer diff --git a/packages/runtime-core/src/compat/global.ts b/packages/runtime-core/src/compat/global.ts index 2efceb87fa6..0379bb67e8e 100644 --- a/packages/runtime-core/src/compat/global.ts +++ b/packages/runtime-core/src/compat/global.ts @@ -17,7 +17,7 @@ import { } from '@vue/shared' import { warn } from '../warning' import { cloneVNode, createVNode } from '../vnode' -import { RootRenderFunction } from '../renderer' +import { ElementNamespace, RootRenderFunction } from '../renderer' import { App, AppConfig, @@ -503,7 +503,13 @@ function installCompatMount( container = selectorOrEl || document.createElement('div') } - const isSVG = container instanceof SVGElement + let namespace: ElementNamespace + if (container instanceof SVGElement) namespace = 'svg' + else if ( + typeof MathMLElement === 'function' && + container instanceof MathMLElement + ) + namespace = 'mathml' // HMR root reload if (__DEV__) { @@ -511,7 +517,7 @@ function installCompatMount( const cloned = cloneVNode(vnode) // compat mode will use instance if not reset to null cloned.component = null - render(cloned, container, isSVG) + render(cloned, container, namespace) } } @@ -538,7 +544,7 @@ function installCompatMount( container.innerHTML = '' // TODO hydration - render(vnode, container, isSVG) + render(vnode, container, namespace) if (container instanceof Element) { container.removeAttribute('v-cloak') diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index 8c1b6318887..8f9a94f1109 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -37,7 +37,8 @@ import { queuePostRenderEffect, MoveType, RendererElement, - RendererNode + RendererNode, + ElementNamespace } from '../renderer' import { setTransitionHooks } from './BaseTransition' import { ComponentRenderContext } from '../componentPublicInstance' @@ -64,7 +65,7 @@ export interface KeepAliveContext extends ComponentRenderContext { vnode: VNode, container: RendererElement, anchor: RendererNode | null, - isSVG: boolean, + namespace: ElementNamespace, optimized: boolean ) => void deactivate: (vnode: VNode) => void @@ -125,7 +126,13 @@ const KeepAliveImpl: ComponentOptions = { } = sharedContext const storageContainer = createElement('div') - sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => { + sharedContext.activate = ( + vnode, + container, + anchor, + namespace, + optimized + ) => { const instance = vnode.component! move(vnode, container, anchor, MoveType.ENTER, parentSuspense) // in case props have changed @@ -136,7 +143,7 @@ const KeepAliveImpl: ComponentOptions = { anchor, instance, parentSuspense, - isSVG, + namespace, vnode.slotScopeIds, optimized ) diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 5e5521b09be..3668418090b 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -18,7 +18,8 @@ import { MoveType, SetupRenderEffectFn, RendererNode, - RendererElement + RendererElement, + ElementNamespace } from '../renderer' import { queuePostFlushCb } from '../scheduler' import { filterSingleRoot, updateHOCHostEl } from '../componentRenderUtils' @@ -63,7 +64,7 @@ export const SuspenseImpl = { anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, - isSVG: boolean, + namespace: ElementNamespace, slotScopeIds: string[] | null, optimized: boolean, // platform-specific impl passed from renderer @@ -76,7 +77,7 @@ export const SuspenseImpl = { anchor, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, optimized, rendererInternals @@ -88,7 +89,7 @@ export const SuspenseImpl = { container, anchor, parentComponent, - isSVG, + namespace, slotScopeIds, optimized, rendererInternals @@ -130,7 +131,7 @@ function mountSuspense( anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, - isSVG: boolean, + namespace: ElementNamespace, slotScopeIds: string[] | null, optimized: boolean, rendererInternals: RendererInternals @@ -147,7 +148,7 @@ function mountSuspense( container, hiddenContainer, anchor, - isSVG, + namespace, slotScopeIds, optimized, rendererInternals @@ -161,7 +162,7 @@ function mountSuspense( null, parentComponent, suspense, - isSVG, + namespace, slotScopeIds ) // now check if we have encountered any async deps @@ -179,7 +180,7 @@ function mountSuspense( anchor, parentComponent, null, // fallback tree will not have suspense context - isSVG, + namespace, slotScopeIds ) setActiveBranch(suspense, vnode.ssFallback!) @@ -195,7 +196,7 @@ function patchSuspense( container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, - isSVG: boolean, + namespace: ElementNamespace, slotScopeIds: string[] | null, optimized: boolean, { p: patch, um: unmount, o: { createElement } }: RendererInternals @@ -218,7 +219,7 @@ function patchSuspense( null, parentComponent, suspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -232,7 +233,7 @@ function patchSuspense( anchor, parentComponent, null, // fallback tree will not have suspense context - isSVG, + namespace, slotScopeIds, optimized ) @@ -267,7 +268,7 @@ function patchSuspense( null, parentComponent, suspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -281,7 +282,7 @@ function patchSuspense( anchor, parentComponent, null, // fallback tree will not have suspense context - isSVG, + namespace, slotScopeIds, optimized ) @@ -296,7 +297,7 @@ function patchSuspense( anchor, parentComponent, suspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -311,7 +312,7 @@ function patchSuspense( null, parentComponent, suspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -330,7 +331,7 @@ function patchSuspense( anchor, parentComponent, suspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -349,7 +350,7 @@ function patchSuspense( null, parentComponent, suspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -376,7 +377,7 @@ export interface SuspenseBoundary { vnode: VNode<RendererNode, RendererElement, SuspenseProps> parent: SuspenseBoundary | null parentComponent: ComponentInternalInstance | null - isSVG: boolean + namespace: ElementNamespace container: RendererElement hiddenContainer: RendererElement anchor: RendererNode | null @@ -413,7 +414,7 @@ function createSuspenseBoundary( container: RendererElement, hiddenContainer: RendererElement, anchor: RendererNode | null, - isSVG: boolean, + namespace: ElementNamespace, slotScopeIds: string[] | null, optimized: boolean, rendererInternals: RendererInternals, @@ -455,7 +456,7 @@ function createSuspenseBoundary( vnode, parent: parentSuspense, parentComponent, - isSVG, + namespace, container, hiddenContainer, anchor, @@ -576,7 +577,7 @@ function createSuspenseBoundary( return } - const { vnode, activeBranch, parentComponent, container, isSVG } = + const { vnode, activeBranch, parentComponent, container, namespace } = suspense // invoke @fallback event @@ -594,7 +595,7 @@ function createSuspenseBoundary( next(activeBranch!), parentComponent, null, // fallback tree will not have suspense context - isSVG, + namespace, slotScopeIds, optimized ) @@ -675,7 +676,7 @@ function createSuspenseBoundary( // consider the comment placeholder case. hydratedEl ? null : next(instance.subTree), suspense, - isSVG, + namespace, optimized ) if (placeholder) { @@ -721,7 +722,7 @@ function hydrateSuspense( vnode: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, - isSVG: boolean, + namespace: ElementNamespace, slotScopeIds: string[] | null, optimized: boolean, rendererInternals: RendererInternals, @@ -742,7 +743,7 @@ function hydrateSuspense( node.parentNode!, document.createElement('div'), null, - isSVG, + namespace, slotScopeIds, optimized, rendererInternals, diff --git a/packages/runtime-core/src/components/Teleport.ts b/packages/runtime-core/src/components/Teleport.ts index d1327db8ee8..aae7deff336 100644 --- a/packages/runtime-core/src/components/Teleport.ts +++ b/packages/runtime-core/src/components/Teleport.ts @@ -6,7 +6,8 @@ import { RendererElement, RendererNode, RendererOptions, - traverseStaticChildren + traverseStaticChildren, + ElementNamespace } from '../renderer' import { VNode, VNodeArrayChildren, VNodeProps } from '../vnode' import { isString, ShapeFlags } from '@vue/shared' @@ -28,6 +29,9 @@ const isTeleportDisabled = (props: VNode['props']): boolean => const isTargetSVG = (target: RendererElement): boolean => typeof SVGElement !== 'undefined' && target instanceof SVGElement +const isTargetMathML = (target: RendererElement): boolean => + typeof MathMLElement === 'function' && target instanceof MathMLElement + const resolveTarget = <T = RendererElement>( props: TeleportProps | null, select: RendererOptions['querySelector'] @@ -72,7 +76,7 @@ export const TeleportImpl = { anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, - isSVG: boolean, + namespace: ElementNamespace, slotScopeIds: string[] | null, optimized: boolean, internals: RendererInternals @@ -109,7 +113,11 @@ export const TeleportImpl = { if (target) { insert(targetAnchor, target) // #2652 we could be teleporting from a non-SVG tree into an SVG tree - isSVG = isSVG || isTargetSVG(target) + if (namespace === 'svg' || isTargetSVG(target)) { + namespace = 'svg' + } else if (namespace === 'mathml' || isTargetMathML(target)) { + namespace = 'mathml' + } } else if (__DEV__ && !disabled) { warn('Invalid Teleport target on mount:', target, `(${typeof target})`) } @@ -124,7 +132,7 @@ export const TeleportImpl = { anchor, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -145,7 +153,12 @@ export const TeleportImpl = { const wasDisabled = isTeleportDisabled(n1.props) const currentContainer = wasDisabled ? container : target const currentAnchor = wasDisabled ? mainAnchor : targetAnchor - isSVG = isSVG || isTargetSVG(target) + + if (namespace === 'svg' || isTargetSVG(target)) { + namespace = 'svg' + } else if (namespace === 'mathml' || isTargetMathML(target)) { + namespace = 'mathml' + } if (dynamicChildren) { // fast path when the teleport happens to be a block root @@ -155,7 +168,7 @@ export const TeleportImpl = { currentContainer, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds ) // even in block tree mode we need to make sure all root-level nodes @@ -170,7 +183,7 @@ export const TeleportImpl = { currentAnchor, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, false ) diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index d79c09d3d36..156327dd880 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -39,7 +39,17 @@ enum DOMNodeTypes { let hasMismatch = false const isSVGContainer = (container: Element) => - /svg/.test(container.namespaceURI!) && container.tagName !== 'foreignObject' + container.namespaceURI!.includes('svg') && + container.tagName !== 'foreignObject' + +const isMathMLContainer = (container: Element) => + container.namespaceURI!.includes('MathML') + +const getContainerType = (container: Element): 'svg' | 'mathml' | undefined => { + if (isSVGContainer(container)) return 'svg' + if (isMathMLContainer(container)) return 'mathml' + return undefined +} const isComment = (node: Node): node is Comment => node.nodeType === DOMNodeTypes.COMMENT @@ -263,7 +273,7 @@ export function createHydrationFunctions( null, parentComponent, parentSuspense, - isSVGContainer(container), + getContainerType(container), optimized ) @@ -306,7 +316,7 @@ export function createHydrationFunctions( vnode, parentComponent, parentSuspense, - isSVGContainer(parentNode(node)!), + getContainerType(parentNode(node)!), slotScopeIds, optimized, rendererInternals, @@ -364,7 +374,7 @@ export function createHydrationFunctions( key, null, props[key], - false, + undefined, undefined, parentComponent ) @@ -378,7 +388,7 @@ export function createHydrationFunctions( 'onClick', null, props.onClick, - false, + undefined, undefined, parentComponent ) @@ -519,7 +529,7 @@ export function createHydrationFunctions( null, parentComponent, parentSuspense, - isSVGContainer(container), + getContainerType(container), slotScopeIds ) } @@ -611,7 +621,7 @@ export function createHydrationFunctions( next, parentComponent, parentSuspense, - isSVGContainer(container), + getContainerType(container), slotScopeIds ) return next diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index fcc460c43d0..f8d21242b83 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -260,7 +260,8 @@ export type { RendererElement, HydrationRenderer, RendererOptions, - RootRenderFunction + RootRenderFunction, + ElementNamespace } from './renderer' export type { RootHydrateFunction } from './hydration' export type { Slot, Slots, SlotsType } from './componentSlots' diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index fc762af3d96..52a3aa1404c 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -83,10 +83,12 @@ export interface HydrationRenderer extends Renderer<Element | ShadowRoot> { hydrate: RootHydrateFunction } +export type ElementNamespace = 'svg' | 'mathml' | undefined + export type RootRenderFunction<HostElement = RendererElement> = ( vnode: VNode | null, container: HostElement, - isSVG?: boolean + namespace?: ElementNamespace ) => void export interface RendererOptions< @@ -98,7 +100,7 @@ export interface RendererOptions< key: string, prevValue: any, nextValue: any, - isSVG?: boolean, + namespace?: ElementNamespace, prevChildren?: VNode<HostNode, HostElement>[], parentComponent?: ComponentInternalInstance | null, parentSuspense?: SuspenseBoundary | null, @@ -108,7 +110,7 @@ export interface RendererOptions< remove(el: HostNode): void createElement( type: string, - isSVG?: boolean, + namespace?: ElementNamespace, isCustomizedBuiltIn?: string, vnodeProps?: (VNodeProps & { [key: string]: any }) | null ): HostElement @@ -125,7 +127,7 @@ export interface RendererOptions< content: string, parent: HostElement, anchor: HostNode | null, - isSVG: boolean, + namespace: ElementNamespace, start?: HostNode | null, end?: HostNode | null ): [HostNode, HostNode] @@ -170,7 +172,7 @@ type PatchFn = ( anchor?: RendererNode | null, parentComponent?: ComponentInternalInstance | null, parentSuspense?: SuspenseBoundary | null, - isSVG?: boolean, + namespace?: ElementNamespace, slotScopeIds?: string[] | null, optimized?: boolean ) => void @@ -181,7 +183,7 @@ type MountChildrenFn = ( anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, - isSVG: boolean, + namespace: ElementNamespace, slotScopeIds: string[] | null, optimized: boolean, start?: number @@ -194,7 +196,7 @@ type PatchChildrenFn = ( anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, - isSVG: boolean, + namespace: ElementNamespace, slotScopeIds: string[] | null, optimized: boolean ) => void @@ -205,7 +207,7 @@ type PatchBlockChildrenFn = ( fallbackContainer: RendererElement, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, - isSVG: boolean, + namespace: ElementNamespace, slotScopeIds: string[] | null ) => void @@ -244,7 +246,7 @@ export type MountComponentFn = ( anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, - isSVG: boolean, + namespace: ElementNamespace, optimized: boolean ) => void @@ -261,7 +263,7 @@ export type SetupRenderEffectFn = ( container: RendererElement, anchor: RendererNode | null, parentSuspense: SuspenseBoundary | null, - isSVG: boolean, + namespace: ElementNamespace, optimized: boolean ) => void @@ -362,7 +364,7 @@ function baseCreateRenderer( anchor = null, parentComponent = null, parentSuspense = null, - isSVG = false, + namespace = undefined, slotScopeIds = null, optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren ) => { @@ -392,9 +394,9 @@ function baseCreateRenderer( break case Static: if (n1 == null) { - mountStaticNode(n2, container, anchor, isSVG) + mountStaticNode(n2, container, anchor, namespace) } else if (__DEV__) { - patchStaticNode(n1, n2, container, isSVG) + patchStaticNode(n1, n2, container, namespace) } break case Fragment: @@ -405,7 +407,7 @@ function baseCreateRenderer( anchor, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -419,7 +421,7 @@ function baseCreateRenderer( anchor, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -431,7 +433,7 @@ function baseCreateRenderer( anchor, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -443,7 +445,7 @@ function baseCreateRenderer( anchor, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, optimized, internals @@ -456,7 +458,7 @@ function baseCreateRenderer( anchor, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, optimized, internals @@ -509,7 +511,7 @@ function baseCreateRenderer( n2: VNode, container: RendererElement, anchor: RendererNode | null, - isSVG: boolean + namespace: ElementNamespace ) => { // static nodes are only present when used with compiler-dom/runtime-dom // which guarantees presence of hostInsertStaticContent. @@ -517,7 +519,7 @@ function baseCreateRenderer( n2.children as string, container, anchor, - isSVG, + namespace, n2.el, n2.anchor ) @@ -530,7 +532,7 @@ function baseCreateRenderer( n1: VNode, n2: VNode, container: RendererElement, - isSVG: boolean + namespace: ElementNamespace ) => { // static nodes are only patched during dev for HMR if (n2.children !== n1.children) { @@ -542,7 +544,7 @@ function baseCreateRenderer( n2.children as string, container, anchor, - isSVG + namespace ) } else { n2.el = n1.el @@ -581,11 +583,16 @@ function baseCreateRenderer( anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, - isSVG: boolean, + namespace: ElementNamespace, slotScopeIds: string[] | null, optimized: boolean ) => { - isSVG = isSVG || n2.type === 'svg' + if (n2.type === 'svg') { + namespace = 'svg' + } else if (n2.type === 'math') { + namespace = 'mathml' + } + if (n1 == null) { mountElement( n2, @@ -593,7 +600,7 @@ function baseCreateRenderer( anchor, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -603,7 +610,7 @@ function baseCreateRenderer( n2, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -616,17 +623,17 @@ function baseCreateRenderer( anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, - isSVG: boolean, + namespace: ElementNamespace, slotScopeIds: string[] | null, optimized: boolean ) => { let el: RendererElement let vnodeHook: VNodeHook | undefined | null - const { type, props, shapeFlag, transition, dirs } = vnode + const { props, shapeFlag, transition, dirs } = vnode el = vnode.el = hostCreateElement( vnode.type as string, - isSVG, + namespace, props && props.is, props ) @@ -642,7 +649,7 @@ function baseCreateRenderer( null, parentComponent, parentSuspense, - isSVG && type !== 'foreignObject', + resolveChildrenNamespace(vnode, namespace), slotScopeIds, optimized ) @@ -662,7 +669,7 @@ function baseCreateRenderer( key, null, props[key], - isSVG, + namespace, vnode.children as VNode[], parentComponent, parentSuspense, @@ -680,7 +687,7 @@ function baseCreateRenderer( * affect non-DOM renderers) */ if ('value' in props) { - hostPatchProp(el, 'value', null, props.value) + hostPatchProp(el, 'value', null, props.value, namespace) } if ((vnodeHook = props.onVnodeBeforeMount)) { invokeVNodeHook(vnodeHook, parentComponent, vnode) @@ -764,7 +771,7 @@ function baseCreateRenderer( anchor, parentComponent, parentSuspense, - isSVG, + namespace: ElementNamespace, slotScopeIds, optimized, start = 0 @@ -780,7 +787,7 @@ function baseCreateRenderer( anchor, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -792,7 +799,7 @@ function baseCreateRenderer( n2: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, - isSVG: boolean, + namespace: ElementNamespace, slotScopeIds: string[] | null, optimized: boolean ) => { @@ -822,7 +829,6 @@ function baseCreateRenderer( dynamicChildren = null } - const areChildrenSVG = isSVG && n2.type !== 'foreignObject' if (dynamicChildren) { patchBlockChildren( n1.dynamicChildren!, @@ -830,7 +836,7 @@ function baseCreateRenderer( el, parentComponent, parentSuspense, - areChildrenSVG, + resolveChildrenNamespace(n2, namespace), slotScopeIds ) if (__DEV__) { @@ -846,7 +852,7 @@ function baseCreateRenderer( null, parentComponent, parentSuspense, - areChildrenSVG, + resolveChildrenNamespace(n2, namespace), slotScopeIds, false ) @@ -866,21 +872,21 @@ function baseCreateRenderer( newProps, parentComponent, parentSuspense, - isSVG + namespace ) } else { // class // this flag is matched when the element has dynamic class bindings. if (patchFlag & PatchFlags.CLASS) { if (oldProps.class !== newProps.class) { - hostPatchProp(el, 'class', null, newProps.class, isSVG) + hostPatchProp(el, 'class', null, newProps.class, namespace) } } // style // this flag is matched when the element has dynamic style bindings if (patchFlag & PatchFlags.STYLE) { - hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG) + hostPatchProp(el, 'style', oldProps.style, newProps.style, namespace) } // props @@ -903,7 +909,7 @@ function baseCreateRenderer( key, prev, next, - isSVG, + namespace, n1.children as VNode[], parentComponent, parentSuspense, @@ -930,7 +936,7 @@ function baseCreateRenderer( newProps, parentComponent, parentSuspense, - isSVG + namespace ) } @@ -949,7 +955,7 @@ function baseCreateRenderer( fallbackContainer, parentComponent, parentSuspense, - isSVG, + namespace: ElementNamespace, slotScopeIds ) => { for (let i = 0; i < newChildren.length; i++) { @@ -979,7 +985,7 @@ function baseCreateRenderer( null, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, true ) @@ -993,7 +999,7 @@ function baseCreateRenderer( newProps: Data, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, - isSVG: boolean + namespace: ElementNamespace ) => { if (oldProps !== newProps) { if (oldProps !== EMPTY_OBJ) { @@ -1004,7 +1010,7 @@ function baseCreateRenderer( key, oldProps[key], null, - isSVG, + namespace, vnode.children as VNode[], parentComponent, parentSuspense, @@ -1025,7 +1031,7 @@ function baseCreateRenderer( key, prev, next, - isSVG, + namespace, vnode.children as VNode[], parentComponent, parentSuspense, @@ -1034,7 +1040,7 @@ function baseCreateRenderer( } } if ('value' in newProps) { - hostPatchProp(el, 'value', oldProps.value, newProps.value) + hostPatchProp(el, 'value', oldProps.value, newProps.value, namespace) } } } @@ -1046,7 +1052,7 @@ function baseCreateRenderer( anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, - isSVG: boolean, + namespace: ElementNamespace, slotScopeIds: string[] | null, optimized: boolean ) => { @@ -1085,7 +1091,7 @@ function baseCreateRenderer( fragmentEndAnchor, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -1106,7 +1112,7 @@ function baseCreateRenderer( container, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds ) if (__DEV__) { @@ -1134,7 +1140,7 @@ function baseCreateRenderer( fragmentEndAnchor, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -1149,7 +1155,7 @@ function baseCreateRenderer( anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, - isSVG: boolean, + namespace: ElementNamespace, slotScopeIds: string[] | null, optimized: boolean ) => { @@ -1160,7 +1166,7 @@ function baseCreateRenderer( n2, container, anchor, - isSVG, + namespace, optimized ) } else { @@ -1170,7 +1176,7 @@ function baseCreateRenderer( anchor, parentComponent, parentSuspense, - isSVG, + namespace, optimized ) } @@ -1185,7 +1191,7 @@ function baseCreateRenderer( anchor, parentComponent, parentSuspense, - isSVG, + namespace: ElementNamespace, optimized ) => { // 2.x compat may pre-create the component instance before actually @@ -1245,7 +1251,7 @@ function baseCreateRenderer( container, anchor, parentSuspense, - isSVG, + namespace, optimized ) @@ -1296,7 +1302,7 @@ function baseCreateRenderer( container, anchor, parentSuspense, - isSVG, + namespace: ElementNamespace, optimized ) => { const componentUpdateFn = () => { @@ -1380,7 +1386,7 @@ function baseCreateRenderer( anchor, instance, parentSuspense, - isSVG + namespace ) if (__DEV__) { endMeasure(instance, `patch`) @@ -1499,7 +1505,7 @@ function baseCreateRenderer( getNextHostNode(prevTree), instance, parentSuspense, - isSVG + namespace ) if (__DEV__) { endMeasure(instance, `patch`) @@ -1599,7 +1605,7 @@ function baseCreateRenderer( anchor, parentComponent, parentSuspense, - isSVG, + namespace: ElementNamespace, slotScopeIds, optimized = false ) => { @@ -1620,7 +1626,7 @@ function baseCreateRenderer( anchor, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -1634,7 +1640,7 @@ function baseCreateRenderer( anchor, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -1663,7 +1669,7 @@ function baseCreateRenderer( anchor, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -1685,7 +1691,7 @@ function baseCreateRenderer( anchor, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -1701,7 +1707,7 @@ function baseCreateRenderer( anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, - isSVG: boolean, + namespace: ElementNamespace, slotScopeIds: string[] | null, optimized: boolean ) => { @@ -1722,7 +1728,7 @@ function baseCreateRenderer( null, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -1745,7 +1751,7 @@ function baseCreateRenderer( anchor, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, optimized, commonLength @@ -1761,7 +1767,7 @@ function baseCreateRenderer( parentAnchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, - isSVG: boolean, + namespace: ElementNamespace, slotScopeIds: string[] | null, optimized: boolean ) => { @@ -1786,7 +1792,7 @@ function baseCreateRenderer( null, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -1812,7 +1818,7 @@ function baseCreateRenderer( null, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -1844,7 +1850,7 @@ function baseCreateRenderer( anchor, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -1947,7 +1953,7 @@ function baseCreateRenderer( null, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -1976,7 +1982,7 @@ function baseCreateRenderer( anchor, parentComponent, parentSuspense, - isSVG, + namespace, slotScopeIds, optimized ) @@ -2321,13 +2327,21 @@ function baseCreateRenderer( return hostNextSibling((vnode.anchor || vnode.el)!) } - const render: RootRenderFunction = (vnode, container, isSVG) => { + const render: RootRenderFunction = (vnode, container, namespace) => { if (vnode == null) { if (container._vnode) { unmount(container._vnode, null, null, true) } } else { - patch(container._vnode || null, vnode, container, null, null, null, isSVG) + patch( + container._vnode || null, + vnode, + container, + null, + null, + null, + namespace + ) } flushPreFlushCbs() flushPostFlushCbs() @@ -2362,6 +2376,20 @@ function baseCreateRenderer( } } +function resolveChildrenNamespace( + { type, props }: VNode, + currentNamespace: ElementNamespace +): ElementNamespace { + return (currentNamespace === 'svg' && type === 'foreignObject') || + (currentNamespace === 'mathml' && + type === 'annotation-xml' && + props && + props.encoding && + props.encoding.includes('html')) + ? undefined + : currentNamespace +} + function toggleRecurse( { effect, update }: ComponentInternalInstance, allowed: boolean diff --git a/packages/runtime-dom/__tests__/nodeOps.spec.ts b/packages/runtime-dom/__tests__/nodeOps.spec.ts index 2573974c955..87adafd6696 100644 --- a/packages/runtime-dom/__tests__/nodeOps.spec.ts +++ b/packages/runtime-dom/__tests__/nodeOps.spec.ts @@ -2,7 +2,7 @@ import { nodeOps, svgNS } from '../src/nodeOps' describe('runtime-dom: node-ops', () => { test("the <select>'s multiple attr should be set in createElement", () => { - const el = nodeOps.createElement('select', false, undefined, { + const el = nodeOps.createElement('select', undefined, undefined, { multiple: '' }) as HTMLSelectElement const option1 = nodeOps.createElement('option') as HTMLOptionElement @@ -21,7 +21,12 @@ describe('runtime-dom: node-ops', () => { test('fresh insertion', () => { const content = `<div>one</div><div>two</div>three` const parent = document.createElement('div') - const nodes = nodeOps.insertStaticContent!(content, parent, null, false) + const nodes = nodeOps.insertStaticContent!( + content, + parent, + null, + undefined + ) expect(parent.innerHTML).toBe(content) expect(nodes[0]).toBe(parent.firstChild) expect(nodes[1]).toBe(parent.lastChild) @@ -33,7 +38,12 @@ describe('runtime-dom: node-ops', () => { const parent = document.createElement('div') parent.innerHTML = existing const anchor = parent.firstChild - const nodes = nodeOps.insertStaticContent!(content, parent, anchor, false) + const nodes = nodeOps.insertStaticContent!( + content, + parent, + anchor, + undefined + ) expect(parent.innerHTML).toBe(content + existing) expect(nodes[0]).toBe(parent.firstChild) expect(nodes[1]).toBe(parent.childNodes[parent.childNodes.length - 2]) @@ -46,7 +56,7 @@ describe('runtime-dom: node-ops', () => { content, parent, null, - true + 'svg' ) expect(parent.innerHTML).toBe(content) expect(first).toBe(parent.firstChild) @@ -65,7 +75,7 @@ describe('runtime-dom: node-ops', () => { content, parent, anchor, - true + 'svg' ) expect(parent.innerHTML).toBe(content + existing) expect(first).toBe(parent.firstChild) @@ -88,7 +98,7 @@ describe('runtime-dom: node-ops', () => { content, parent, anchor, - false, + undefined, cached.firstChild, cached.lastChild ) diff --git a/packages/runtime-dom/__tests__/patchAttrs.spec.ts b/packages/runtime-dom/__tests__/patchAttrs.spec.ts index b78dd44c634..f06fb4a5ea6 100644 --- a/packages/runtime-dom/__tests__/patchAttrs.spec.ts +++ b/packages/runtime-dom/__tests__/patchAttrs.spec.ts @@ -4,15 +4,15 @@ import { xlinkNS } from '../src/modules/attrs' describe('runtime-dom: attrs patching', () => { test('xlink attributes', () => { const el = document.createElementNS('http://www.w3.org/2000/svg', 'use') - patchProp(el, 'xlink:href', null, 'a', true) + patchProp(el, 'xlink:href', null, 'a', 'svg') expect(el.getAttributeNS(xlinkNS, 'href')).toBe('a') - patchProp(el, 'xlink:href', 'a', null, true) + patchProp(el, 'xlink:href', 'a', null, 'svg') expect(el.getAttributeNS(xlinkNS, 'href')).toBe(null) }) test('textContent attributes /w svg', () => { const el = document.createElementNS('http://www.w3.org/2000/svg', 'use') - patchProp(el, 'textContent', null, 'foo', true) + patchProp(el, 'textContent', null, 'foo', 'svg') expect(el.attributes.length).toBe(0) expect(el.innerHTML).toBe('foo') }) diff --git a/packages/runtime-dom/__tests__/patchClass.spec.ts b/packages/runtime-dom/__tests__/patchClass.spec.ts index c8da741677a..7af4c885cbe 100644 --- a/packages/runtime-dom/__tests__/patchClass.spec.ts +++ b/packages/runtime-dom/__tests__/patchClass.spec.ts @@ -25,7 +25,7 @@ describe('runtime-dom: class patching', () => { test('svg', () => { const el = document.createElementNS(svgNS, 'svg') - patchProp(el, 'class', null, 'foo', true) + patchProp(el, 'class', null, 'foo', 'svg') expect(el.getAttribute('class')).toBe('foo') }) }) diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index ad818a34bb7..124789100e2 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -10,7 +10,8 @@ import { RootHydrateFunction, isRuntimeOnly, DeprecationTypes, - compatUtils + compatUtils, + ElementNamespace } from '@vue/runtime-core' import { nodeOps } from './nodeOps' import { patchProp } from './patchProp' @@ -21,7 +22,8 @@ import { isHTMLTag, isSVGTag, extend, - NOOP + NOOP, + isMathMLTag } from '@vue/shared' declare module '@vue/reactivity' { @@ -99,7 +101,7 @@ export const createApp = ((...args) => { // clear content before mounting container.innerHTML = '' - const proxy = mount(container, false, container instanceof SVGElement) + const proxy = mount(container, false, resolveRootNamespace(container)) if (container instanceof Element) { container.removeAttribute('v-cloak') container.setAttribute('data-v-app', '') @@ -122,18 +124,30 @@ export const createSSRApp = ((...args) => { app.mount = (containerOrSelector: Element | ShadowRoot | string): any => { const container = normalizeContainer(containerOrSelector) if (container) { - return mount(container, true, container instanceof SVGElement) + return mount(container, true, resolveRootNamespace(container)) } } return app }) as CreateAppFunction<Element> +function resolveRootNamespace(container: Element): ElementNamespace { + if (container instanceof SVGElement) { + return 'svg' + } + if ( + typeof MathMLElement === 'function' && + container instanceof MathMLElement + ) { + return 'mathml' + } +} + function injectNativeTagCheck(app: App) { // Inject `isNativeTag` // this is used for component name validation (dev only) Object.defineProperty(app.config, 'isNativeTag', { - value: (tag: string) => isHTMLTag(tag) || isSVGTag(tag), + value: (tag: string) => isHTMLTag(tag) || isSVGTag(tag) || isMathMLTag(tag), writable: false }) } diff --git a/packages/runtime-dom/src/nodeOps.ts b/packages/runtime-dom/src/nodeOps.ts index daf56fc6f3a..b9595536d00 100644 --- a/packages/runtime-dom/src/nodeOps.ts +++ b/packages/runtime-dom/src/nodeOps.ts @@ -1,6 +1,7 @@ import { RendererOptions } from '@vue/runtime-core' export const svgNS = 'http://www.w3.org/2000/svg' +export const mathmlNS = 'http://www.w3.org/1998/Math/MathML' const doc = (typeof document !== 'undefined' ? document : null) as Document @@ -18,10 +19,13 @@ export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = { } }, - createElement: (tag, isSVG, is, props): Element => { - const el = isSVG - ? doc.createElementNS(svgNS, tag) - : doc.createElement(tag, is ? { is } : undefined) + createElement: (tag, namespace, is, props): Element => { + const el = + namespace === 'svg' + ? doc.createElementNS(svgNS, tag) + : namespace === 'mathml' + ? doc.createElementNS(mathmlNS, tag) + : doc.createElement(tag, is ? { is } : undefined) if (tag === 'select' && props && props.multiple != null) { ;(el as HTMLSelectElement).setAttribute('multiple', props.multiple) @@ -56,7 +60,7 @@ export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = { // Reason: innerHTML. // Static content here can only come from compiled templates. // As long as the user only uses trusted templates, this is safe. - insertStaticContent(content, parent, anchor, isSVG, start, end) { + insertStaticContent(content, parent, anchor, namespace, start, end) { // <parent> before | first ... last | anchor </parent> const before = anchor ? anchor.previousSibling : parent.lastChild // #5308 can only take cached path if: @@ -70,10 +74,16 @@ export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = { } } else { // fresh insert - templateContainer.innerHTML = isSVG ? `<svg>${content}</svg>` : content + templateContainer.innerHTML = + namespace === 'svg' + ? `<svg>${content}</svg>` + : namespace === 'mathml' + ? `<math>${content}</math>` + : content + const template = templateContainer.content - if (isSVG) { - // remove outer svg wrapper + if (namespace === 'svg' || namespace === 'mathml') { + // remove outer svg/math wrapper const wrapper = template.firstChild! while (wrapper.firstChild) { template.appendChild(wrapper.firstChild) diff --git a/packages/runtime-dom/src/patchProp.ts b/packages/runtime-dom/src/patchProp.ts index e5883d18fca..d0f6923c5b4 100644 --- a/packages/runtime-dom/src/patchProp.ts +++ b/packages/runtime-dom/src/patchProp.ts @@ -20,12 +20,13 @@ export const patchProp: DOMRendererOptions['patchProp'] = ( key, prevValue, nextValue, - isSVG = false, + namespace, prevChildren, parentComponent, parentSuspense, unmountChildren ) => { + const isSVG = namespace === 'svg' if (key === 'class') { patchClass(el, nextValue, isSVG) } else if (key === 'style') { diff --git a/packages/shared/src/domTagConfig.ts b/packages/shared/src/domTagConfig.ts index 535aa6be718..4cbb02b5f79 100644 --- a/packages/shared/src/domTagConfig.ts +++ b/packages/shared/src/domTagConfig.ts @@ -27,6 +27,13 @@ const SVG_TAGS = 'polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,' + 'text,textPath,title,tspan,unknown,use,view' +// https://developer.mozilla.org/en-US/docs/Web/MathML/Element +const MATH_TAGS = + 'math,maction,annotation,annotation-xml,menclose,merror,mfenced,mfrac,mi,' + + 'mmultiscripts,mn,mo,mover,mpadded,mphantom,mprescripts,mroot,mrow,ms,' + + 'semantics,mspace,msqrt,mstyle,msub,msup,msubsup,mtable,mtd,mtext,mtr,' + + 'munder,munderover' + const VOID_TAGS = 'area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr' @@ -40,6 +47,11 @@ export const isHTMLTag = /*#__PURE__*/ makeMap(HTML_TAGS) * Do NOT use in runtime code paths unless behind `__DEV__` flag. */ export const isSVGTag = /*#__PURE__*/ makeMap(SVG_TAGS) +/** + * Compiler only. + * Do NOT use in runtime code paths unless behind `__DEV__` flag. + */ +export const isMathMLTag = /*#__PURE__*/ makeMap(MATH_TAGS) /** * Compiler only. * Do NOT use in runtime code paths unless behind `__DEV__` flag. diff --git a/packages/vue/__tests__/mathmlNamespace.spec.ts b/packages/vue/__tests__/mathmlNamespace.spec.ts new file mode 100644 index 00000000000..3fe1221f534 --- /dev/null +++ b/packages/vue/__tests__/mathmlNamespace.spec.ts @@ -0,0 +1,80 @@ +// MathML logic is technically dom-specific, but the logic is placed in core +// because splitting it out of core would lead to unnecessary complexity in both +// the renderer and compiler implementations. +// Related files: +// - runtime-core/src/renderer.ts +// - compiler-core/src/transforms/transformElement.ts + +import { vtcKey } from '../../runtime-dom/src/components/Transition' +import { render, h, ref, nextTick } from '../src' + +describe('MathML support', () => { + afterEach(() => { + document.body.innerHTML = '' + }) + + test('should mount elements with correct html namespace', () => { + const root = document.createElement('div') + document.body.appendChild(root) + const App = { + template: ` + <math display="block" id="e0"> + <semantics id="e1"> + <mrow id="e2"> + <msup> + <mi>x</mi> + <mn>2</mn> + </msup> + <mo>+</mo> + <mi>y</mi> + </mrow> + + <annotation-xml encoding="text/html" id="e3"> + <div id="e4" /> + <svg id="e5" /> + </annotation-xml> + </semantics> + </math> + ` + } + render(h(App), root) + const e0 = document.getElementById('e0')! + expect(e0.namespaceURI).toMatch('Math') + expect(e0.querySelector('#e1')!.namespaceURI).toMatch('Math') + expect(e0.querySelector('#e2')!.namespaceURI).toMatch('Math') + expect(e0.querySelector('#e3')!.namespaceURI).toMatch('Math') + expect(e0.querySelector('#e4')!.namespaceURI).toMatch('xhtml') + expect(e0.querySelector('#e5')!.namespaceURI).toMatch('svg') + }) + + test('should patch elements with correct namespaces', async () => { + const root = document.createElement('div') + document.body.appendChild(root) + const cls = ref('foo') + const App = { + setup: () => ({ cls }), + template: ` + <div> + <math id="f1" :class="cls"> + <annotation encoding="text/html"> + <div id="f2" :class="cls"/> + </annotation> + </math> + </div> + ` + } + render(h(App), root) + const f1 = document.querySelector('#f1')! + const f2 = document.querySelector('#f2')! + expect(f1.getAttribute('class')).toBe('foo') + expect(f2.className).toBe('foo') + + // set a transition class on the <div> - which is only respected on non-svg + // patches + ;(f2 as any)[vtcKey] = ['baz'] + cls.value = 'bar' + await nextTick() + expect(f1.getAttribute('class')).toBe('bar') + expect(f2.className).toBe('bar baz') + }) +}) diff --git a/packages/vue/__tests__/svgNamespace.spec.ts b/packages/vue/__tests__/svgNamespace.spec.ts index e944e7d8663..df52bbf60a8 100644 --- a/packages/vue/__tests__/svgNamespace.spec.ts +++ b/packages/vue/__tests__/svgNamespace.spec.ts @@ -9,7 +9,11 @@ import { vtcKey } from '../../runtime-dom/src/components/Transition' import { render, h, ref, nextTick } from '../src' describe('SVG support', () => { - test('should mount elements with correct namespaces', () => { + afterEach(() => { + document.body.innerHTML = '' + }) + + test('should mount elements with correct html namespace', () => { const root = document.createElement('div') document.body.appendChild(root) const App = { @@ -18,6 +22,8 @@ describe('SVG support', () => { <svg id="e1"> <foreignObject id="e2"> <div id="e3"/> + <svg id="e4"/> + <math id="e5"/> </foreignObject> </svg> </div> @@ -29,6 +35,8 @@ describe('SVG support', () => { expect(e0.querySelector('#e1')!.namespaceURI).toMatch('svg') expect(e0.querySelector('#e2')!.namespaceURI).toMatch('svg') expect(e0.querySelector('#e3')!.namespaceURI).toMatch('xhtml') + expect(e0.querySelector('#e4')!.namespaceURI).toMatch('svg') + expect(e0.querySelector('#e5')!.namespaceURI).toMatch('Math') }) test('should patch elements with correct namespaces', async () => { diff --git a/scripts/setupVitest.ts b/scripts/setupVitest.ts index cd1e672fd28..41df611a267 100644 --- a/scripts/setupVitest.ts +++ b/scripts/setupVitest.ts @@ -1,5 +1,7 @@ import { type SpyInstance } from 'vitest' +vi.stubGlobal('MathMLElement', class MathMLElement {}) + expect.extend({ toHaveBeenWarned(received: string) { asserted.add(received)