feat: MathML support (#7836)

close #7820
This commit is contained in:
Fabian Gündel 2023-12-08 11:25:01 +01:00 committed by GitHub
parent bc7698dbfe
commit d42b6ba3f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 372 additions and 157 deletions

View File

@ -1,12 +1,12 @@
import { ParserOptions, NodeTypes, Namespaces } from '@vue/compiler-core' 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 { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
import { decodeHtmlBrowser } from './decodeHtmlBrowser' import { decodeHtmlBrowser } from './decodeHtmlBrowser'
export const parserOptions: ParserOptions = { export const parserOptions: ParserOptions = {
parseMode: 'html', parseMode: 'html',
isVoidTag, isVoidTag,
isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag), isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag) || isMathMLTag(tag),
isPreTag: tag => tag === 'pre', isPreTag: tag => tag === 'pre',
decodeEntities: __BROWSER__ ? decodeHtmlBrowser : undefined, decodeEntities: __BROWSER__ ? decodeHtmlBrowser : undefined,

View File

@ -16,7 +16,7 @@ import {
ComponentPublicInstance ComponentPublicInstance
} from './componentPublicInstance' } from './componentPublicInstance'
import { Directive, validateDirectiveName } from './directives' import { Directive, validateDirectiveName } from './directives'
import { RootRenderFunction } from './renderer' import { ElementNamespace, RootRenderFunction } from './renderer'
import { InjectionKey } from './apiInject' import { InjectionKey } from './apiInject'
import { warn } from './warning' import { warn } from './warning'
import { createVNode, cloneVNode, VNode } from './vnode' import { createVNode, cloneVNode, VNode } from './vnode'
@ -47,7 +47,7 @@ export interface App<HostElement = any> {
mount( mount(
rootContainer: HostElement | string, rootContainer: HostElement | string,
isHydrate?: boolean, isHydrate?: boolean,
isSVG?: boolean namespace?: boolean | ElementNamespace
): ComponentPublicInstance ): ComponentPublicInstance
unmount(): void unmount(): void
provide<T>(key: InjectionKey<T> | string, value: T): this provide<T>(key: InjectionKey<T> | string, value: T): this
@ -297,7 +297,7 @@ export function createAppAPI<HostElement>(
mount( mount(
rootContainer: HostElement, rootContainer: HostElement,
isHydrate?: boolean, isHydrate?: boolean,
isSVG?: boolean namespace?: boolean | ElementNamespace
): any { ): any {
if (!isMounted) { if (!isMounted) {
// #5571 // #5571
@ -313,17 +313,29 @@ export function createAppAPI<HostElement>(
// this will be set on the root instance on initial mount. // this will be set on the root instance on initial mount.
vnode.appContext = context vnode.appContext = context
if (namespace === true) {
namespace = 'svg'
} else if (namespace === false) {
namespace = undefined
}
// HMR root reload // HMR root reload
if (__DEV__) { if (__DEV__) {
context.reload = () => { 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) { if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any) hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else { } else {
render(vnode, rootContainer, isSVG) render(vnode, rootContainer, namespace)
} }
isMounted = true isMounted = true
app._container = rootContainer app._container = rootContainer

View File

@ -17,7 +17,7 @@ import {
} from '@vue/shared' } from '@vue/shared'
import { warn } from '../warning' import { warn } from '../warning'
import { cloneVNode, createVNode } from '../vnode' import { cloneVNode, createVNode } from '../vnode'
import { RootRenderFunction } from '../renderer' import { ElementNamespace, RootRenderFunction } from '../renderer'
import { import {
App, App,
AppConfig, AppConfig,
@ -503,7 +503,13 @@ function installCompatMount(
container = selectorOrEl || document.createElement('div') 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 // HMR root reload
if (__DEV__) { if (__DEV__) {
@ -511,7 +517,7 @@ function installCompatMount(
const cloned = cloneVNode(vnode) const cloned = cloneVNode(vnode)
// compat mode will use instance if not reset to null // compat mode will use instance if not reset to null
cloned.component = null cloned.component = null
render(cloned, container, isSVG) render(cloned, container, namespace)
} }
} }
@ -538,7 +544,7 @@ function installCompatMount(
container.innerHTML = '' container.innerHTML = ''
// TODO hydration // TODO hydration
render(vnode, container, isSVG) render(vnode, container, namespace)
if (container instanceof Element) { if (container instanceof Element) {
container.removeAttribute('v-cloak') container.removeAttribute('v-cloak')

View File

@ -37,7 +37,8 @@ import {
queuePostRenderEffect, queuePostRenderEffect,
MoveType, MoveType,
RendererElement, RendererElement,
RendererNode RendererNode,
ElementNamespace
} from '../renderer' } from '../renderer'
import { setTransitionHooks } from './BaseTransition' import { setTransitionHooks } from './BaseTransition'
import { ComponentRenderContext } from '../componentPublicInstance' import { ComponentRenderContext } from '../componentPublicInstance'
@ -64,7 +65,7 @@ export interface KeepAliveContext extends ComponentRenderContext {
vnode: VNode, vnode: VNode,
container: RendererElement, container: RendererElement,
anchor: RendererNode | null, anchor: RendererNode | null,
isSVG: boolean, namespace: ElementNamespace,
optimized: boolean optimized: boolean
) => void ) => void
deactivate: (vnode: VNode) => void deactivate: (vnode: VNode) => void
@ -125,7 +126,13 @@ const KeepAliveImpl: ComponentOptions = {
} = sharedContext } = sharedContext
const storageContainer = createElement('div') const storageContainer = createElement('div')
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => { sharedContext.activate = (
vnode,
container,
anchor,
namespace,
optimized
) => {
const instance = vnode.component! const instance = vnode.component!
move(vnode, container, anchor, MoveType.ENTER, parentSuspense) move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
// in case props have changed // in case props have changed
@ -136,7 +143,7 @@ const KeepAliveImpl: ComponentOptions = {
anchor, anchor,
instance, instance,
parentSuspense, parentSuspense,
isSVG, namespace,
vnode.slotScopeIds, vnode.slotScopeIds,
optimized optimized
) )

View File

@ -18,7 +18,8 @@ import {
MoveType, MoveType,
SetupRenderEffectFn, SetupRenderEffectFn,
RendererNode, RendererNode,
RendererElement RendererElement,
ElementNamespace
} from '../renderer' } from '../renderer'
import { queuePostFlushCb } from '../scheduler' import { queuePostFlushCb } from '../scheduler'
import { filterSingleRoot, updateHOCHostEl } from '../componentRenderUtils' import { filterSingleRoot, updateHOCHostEl } from '../componentRenderUtils'
@ -63,7 +64,7 @@ export const SuspenseImpl = {
anchor: RendererNode | null, anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null, parentSuspense: SuspenseBoundary | null,
isSVG: boolean, namespace: ElementNamespace,
slotScopeIds: string[] | null, slotScopeIds: string[] | null,
optimized: boolean, optimized: boolean,
// platform-specific impl passed from renderer // platform-specific impl passed from renderer
@ -76,7 +77,7 @@ export const SuspenseImpl = {
anchor, anchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized, optimized,
rendererInternals rendererInternals
@ -88,7 +89,7 @@ export const SuspenseImpl = {
container, container,
anchor, anchor,
parentComponent, parentComponent,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized, optimized,
rendererInternals rendererInternals
@ -130,7 +131,7 @@ function mountSuspense(
anchor: RendererNode | null, anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null, parentSuspense: SuspenseBoundary | null,
isSVG: boolean, namespace: ElementNamespace,
slotScopeIds: string[] | null, slotScopeIds: string[] | null,
optimized: boolean, optimized: boolean,
rendererInternals: RendererInternals rendererInternals: RendererInternals
@ -147,7 +148,7 @@ function mountSuspense(
container, container,
hiddenContainer, hiddenContainer,
anchor, anchor,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized, optimized,
rendererInternals rendererInternals
@ -161,7 +162,7 @@ function mountSuspense(
null, null,
parentComponent, parentComponent,
suspense, suspense,
isSVG, namespace,
slotScopeIds slotScopeIds
) )
// now check if we have encountered any async deps // now check if we have encountered any async deps
@ -179,7 +180,7 @@ function mountSuspense(
anchor, anchor,
parentComponent, parentComponent,
null, // fallback tree will not have suspense context null, // fallback tree will not have suspense context
isSVG, namespace,
slotScopeIds slotScopeIds
) )
setActiveBranch(suspense, vnode.ssFallback!) setActiveBranch(suspense, vnode.ssFallback!)
@ -195,7 +196,7 @@ function patchSuspense(
container: RendererElement, container: RendererElement,
anchor: RendererNode | null, anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
isSVG: boolean, namespace: ElementNamespace,
slotScopeIds: string[] | null, slotScopeIds: string[] | null,
optimized: boolean, optimized: boolean,
{ p: patch, um: unmount, o: { createElement } }: RendererInternals { p: patch, um: unmount, o: { createElement } }: RendererInternals
@ -218,7 +219,7 @@ function patchSuspense(
null, null,
parentComponent, parentComponent,
suspense, suspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -232,7 +233,7 @@ function patchSuspense(
anchor, anchor,
parentComponent, parentComponent,
null, // fallback tree will not have suspense context null, // fallback tree will not have suspense context
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -267,7 +268,7 @@ function patchSuspense(
null, null,
parentComponent, parentComponent,
suspense, suspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -281,7 +282,7 @@ function patchSuspense(
anchor, anchor,
parentComponent, parentComponent,
null, // fallback tree will not have suspense context null, // fallback tree will not have suspense context
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -296,7 +297,7 @@ function patchSuspense(
anchor, anchor,
parentComponent, parentComponent,
suspense, suspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -311,7 +312,7 @@ function patchSuspense(
null, null,
parentComponent, parentComponent,
suspense, suspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -330,7 +331,7 @@ function patchSuspense(
anchor, anchor,
parentComponent, parentComponent,
suspense, suspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -349,7 +350,7 @@ function patchSuspense(
null, null,
parentComponent, parentComponent,
suspense, suspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -376,7 +377,7 @@ export interface SuspenseBoundary {
vnode: VNode<RendererNode, RendererElement, SuspenseProps> vnode: VNode<RendererNode, RendererElement, SuspenseProps>
parent: SuspenseBoundary | null parent: SuspenseBoundary | null
parentComponent: ComponentInternalInstance | null parentComponent: ComponentInternalInstance | null
isSVG: boolean namespace: ElementNamespace
container: RendererElement container: RendererElement
hiddenContainer: RendererElement hiddenContainer: RendererElement
anchor: RendererNode | null anchor: RendererNode | null
@ -413,7 +414,7 @@ function createSuspenseBoundary(
container: RendererElement, container: RendererElement,
hiddenContainer: RendererElement, hiddenContainer: RendererElement,
anchor: RendererNode | null, anchor: RendererNode | null,
isSVG: boolean, namespace: ElementNamespace,
slotScopeIds: string[] | null, slotScopeIds: string[] | null,
optimized: boolean, optimized: boolean,
rendererInternals: RendererInternals, rendererInternals: RendererInternals,
@ -455,7 +456,7 @@ function createSuspenseBoundary(
vnode, vnode,
parent: parentSuspense, parent: parentSuspense,
parentComponent, parentComponent,
isSVG, namespace,
container, container,
hiddenContainer, hiddenContainer,
anchor, anchor,
@ -576,7 +577,7 @@ function createSuspenseBoundary(
return return
} }
const { vnode, activeBranch, parentComponent, container, isSVG } = const { vnode, activeBranch, parentComponent, container, namespace } =
suspense suspense
// invoke @fallback event // invoke @fallback event
@ -594,7 +595,7 @@ function createSuspenseBoundary(
next(activeBranch!), next(activeBranch!),
parentComponent, parentComponent,
null, // fallback tree will not have suspense context null, // fallback tree will not have suspense context
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -675,7 +676,7 @@ function createSuspenseBoundary(
// consider the comment placeholder case. // consider the comment placeholder case.
hydratedEl ? null : next(instance.subTree), hydratedEl ? null : next(instance.subTree),
suspense, suspense,
isSVG, namespace,
optimized optimized
) )
if (placeholder) { if (placeholder) {
@ -721,7 +722,7 @@ function hydrateSuspense(
vnode: VNode, vnode: VNode,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null, parentSuspense: SuspenseBoundary | null,
isSVG: boolean, namespace: ElementNamespace,
slotScopeIds: string[] | null, slotScopeIds: string[] | null,
optimized: boolean, optimized: boolean,
rendererInternals: RendererInternals, rendererInternals: RendererInternals,
@ -742,7 +743,7 @@ function hydrateSuspense(
node.parentNode!, node.parentNode!,
document.createElement('div'), document.createElement('div'),
null, null,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized, optimized,
rendererInternals, rendererInternals,

View File

@ -6,7 +6,8 @@ import {
RendererElement, RendererElement,
RendererNode, RendererNode,
RendererOptions, RendererOptions,
traverseStaticChildren traverseStaticChildren,
ElementNamespace
} from '../renderer' } from '../renderer'
import { VNode, VNodeArrayChildren, VNodeProps } from '../vnode' import { VNode, VNodeArrayChildren, VNodeProps } from '../vnode'
import { isString, ShapeFlags } from '@vue/shared' import { isString, ShapeFlags } from '@vue/shared'
@ -28,6 +29,9 @@ const isTeleportDisabled = (props: VNode['props']): boolean =>
const isTargetSVG = (target: RendererElement): boolean => const isTargetSVG = (target: RendererElement): boolean =>
typeof SVGElement !== 'undefined' && target instanceof SVGElement typeof SVGElement !== 'undefined' && target instanceof SVGElement
const isTargetMathML = (target: RendererElement): boolean =>
typeof MathMLElement === 'function' && target instanceof MathMLElement
const resolveTarget = <T = RendererElement>( const resolveTarget = <T = RendererElement>(
props: TeleportProps | null, props: TeleportProps | null,
select: RendererOptions['querySelector'] select: RendererOptions['querySelector']
@ -72,7 +76,7 @@ export const TeleportImpl = {
anchor: RendererNode | null, anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null, parentSuspense: SuspenseBoundary | null,
isSVG: boolean, namespace: ElementNamespace,
slotScopeIds: string[] | null, slotScopeIds: string[] | null,
optimized: boolean, optimized: boolean,
internals: RendererInternals internals: RendererInternals
@ -109,7 +113,11 @@ export const TeleportImpl = {
if (target) { if (target) {
insert(targetAnchor, target) insert(targetAnchor, target)
// #2652 we could be teleporting from a non-SVG tree into an SVG tree // #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) { } else if (__DEV__ && !disabled) {
warn('Invalid Teleport target on mount:', target, `(${typeof target})`) warn('Invalid Teleport target on mount:', target, `(${typeof target})`)
} }
@ -124,7 +132,7 @@ export const TeleportImpl = {
anchor, anchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -145,7 +153,12 @@ export const TeleportImpl = {
const wasDisabled = isTeleportDisabled(n1.props) const wasDisabled = isTeleportDisabled(n1.props)
const currentContainer = wasDisabled ? container : target const currentContainer = wasDisabled ? container : target
const currentAnchor = wasDisabled ? mainAnchor : targetAnchor 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) { if (dynamicChildren) {
// fast path when the teleport happens to be a block root // fast path when the teleport happens to be a block root
@ -155,7 +168,7 @@ export const TeleportImpl = {
currentContainer, currentContainer,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds slotScopeIds
) )
// even in block tree mode we need to make sure all root-level nodes // even in block tree mode we need to make sure all root-level nodes
@ -170,7 +183,7 @@ export const TeleportImpl = {
currentAnchor, currentAnchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
false false
) )

View File

@ -52,7 +52,17 @@ enum DOMNodeTypes {
let hasMismatch = false let hasMismatch = false
const isSVGContainer = (container: Element) => 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 => const isComment = (node: Node): node is Comment =>
node.nodeType === DOMNodeTypes.COMMENT node.nodeType === DOMNodeTypes.COMMENT
@ -277,7 +287,7 @@ export function createHydrationFunctions(
null, null,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVGContainer(container), getContainerType(container),
optimized optimized
) )
@ -320,7 +330,7 @@ export function createHydrationFunctions(
vnode, vnode,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVGContainer(parentNode(node)!), getContainerType(parentNode(node)!),
slotScopeIds, slotScopeIds,
optimized, optimized,
rendererInternals, rendererInternals,
@ -453,7 +463,7 @@ export function createHydrationFunctions(
key, key,
null, null,
props[key], props[key],
false, undefined,
undefined, undefined,
parentComponent parentComponent
) )
@ -467,7 +477,7 @@ export function createHydrationFunctions(
'onClick', 'onClick',
null, null,
props.onClick, props.onClick,
false, undefined,
undefined, undefined,
parentComponent parentComponent
) )
@ -547,7 +557,7 @@ export function createHydrationFunctions(
null, null,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVGContainer(container), getContainerType(container),
slotScopeIds slotScopeIds
) )
} }
@ -639,7 +649,7 @@ export function createHydrationFunctions(
next, next,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVGContainer(container), getContainerType(container),
slotScopeIds slotScopeIds
) )
return next return next

View File

@ -260,7 +260,8 @@ export type {
RendererElement, RendererElement,
HydrationRenderer, HydrationRenderer,
RendererOptions, RendererOptions,
RootRenderFunction RootRenderFunction,
ElementNamespace
} from './renderer' } from './renderer'
export type { RootHydrateFunction } from './hydration' export type { RootHydrateFunction } from './hydration'
export type { Slot, Slots, SlotsType } from './componentSlots' export type { Slot, Slots, SlotsType } from './componentSlots'

View File

@ -83,10 +83,12 @@ export interface HydrationRenderer extends Renderer<Element | ShadowRoot> {
hydrate: RootHydrateFunction hydrate: RootHydrateFunction
} }
export type ElementNamespace = 'svg' | 'mathml' | undefined
export type RootRenderFunction<HostElement = RendererElement> = ( export type RootRenderFunction<HostElement = RendererElement> = (
vnode: VNode | null, vnode: VNode | null,
container: HostElement, container: HostElement,
isSVG?: boolean namespace?: ElementNamespace
) => void ) => void
export interface RendererOptions< export interface RendererOptions<
@ -98,7 +100,7 @@ export interface RendererOptions<
key: string, key: string,
prevValue: any, prevValue: any,
nextValue: any, nextValue: any,
isSVG?: boolean, namespace?: ElementNamespace,
prevChildren?: VNode<HostNode, HostElement>[], prevChildren?: VNode<HostNode, HostElement>[],
parentComponent?: ComponentInternalInstance | null, parentComponent?: ComponentInternalInstance | null,
parentSuspense?: SuspenseBoundary | null, parentSuspense?: SuspenseBoundary | null,
@ -108,7 +110,7 @@ export interface RendererOptions<
remove(el: HostNode): void remove(el: HostNode): void
createElement( createElement(
type: string, type: string,
isSVG?: boolean, namespace?: ElementNamespace,
isCustomizedBuiltIn?: string, isCustomizedBuiltIn?: string,
vnodeProps?: (VNodeProps & { [key: string]: any }) | null vnodeProps?: (VNodeProps & { [key: string]: any }) | null
): HostElement ): HostElement
@ -125,7 +127,7 @@ export interface RendererOptions<
content: string, content: string,
parent: HostElement, parent: HostElement,
anchor: HostNode | null, anchor: HostNode | null,
isSVG: boolean, namespace: ElementNamespace,
start?: HostNode | null, start?: HostNode | null,
end?: HostNode | null end?: HostNode | null
): [HostNode, HostNode] ): [HostNode, HostNode]
@ -170,7 +172,7 @@ type PatchFn = (
anchor?: RendererNode | null, anchor?: RendererNode | null,
parentComponent?: ComponentInternalInstance | null, parentComponent?: ComponentInternalInstance | null,
parentSuspense?: SuspenseBoundary | null, parentSuspense?: SuspenseBoundary | null,
isSVG?: boolean, namespace?: ElementNamespace,
slotScopeIds?: string[] | null, slotScopeIds?: string[] | null,
optimized?: boolean optimized?: boolean
) => void ) => void
@ -181,7 +183,7 @@ type MountChildrenFn = (
anchor: RendererNode | null, anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null, parentSuspense: SuspenseBoundary | null,
isSVG: boolean, namespace: ElementNamespace,
slotScopeIds: string[] | null, slotScopeIds: string[] | null,
optimized: boolean, optimized: boolean,
start?: number start?: number
@ -194,7 +196,7 @@ type PatchChildrenFn = (
anchor: RendererNode | null, anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null, parentSuspense: SuspenseBoundary | null,
isSVG: boolean, namespace: ElementNamespace,
slotScopeIds: string[] | null, slotScopeIds: string[] | null,
optimized: boolean optimized: boolean
) => void ) => void
@ -205,7 +207,7 @@ type PatchBlockChildrenFn = (
fallbackContainer: RendererElement, fallbackContainer: RendererElement,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null, parentSuspense: SuspenseBoundary | null,
isSVG: boolean, namespace: ElementNamespace,
slotScopeIds: string[] | null slotScopeIds: string[] | null
) => void ) => void
@ -244,7 +246,7 @@ export type MountComponentFn = (
anchor: RendererNode | null, anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null, parentSuspense: SuspenseBoundary | null,
isSVG: boolean, namespace: ElementNamespace,
optimized: boolean optimized: boolean
) => void ) => void
@ -261,7 +263,7 @@ export type SetupRenderEffectFn = (
container: RendererElement, container: RendererElement,
anchor: RendererNode | null, anchor: RendererNode | null,
parentSuspense: SuspenseBoundary | null, parentSuspense: SuspenseBoundary | null,
isSVG: boolean, namespace: ElementNamespace,
optimized: boolean optimized: boolean
) => void ) => void
@ -362,7 +364,7 @@ function baseCreateRenderer(
anchor = null, anchor = null,
parentComponent = null, parentComponent = null,
parentSuspense = null, parentSuspense = null,
isSVG = false, namespace = undefined,
slotScopeIds = null, slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => { ) => {
@ -392,9 +394,9 @@ function baseCreateRenderer(
break break
case Static: case Static:
if (n1 == null) { if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG) mountStaticNode(n2, container, anchor, namespace)
} else if (__DEV__) { } else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG) patchStaticNode(n1, n2, container, namespace)
} }
break break
case Fragment: case Fragment:
@ -405,7 +407,7 @@ function baseCreateRenderer(
anchor, anchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -419,7 +421,7 @@ function baseCreateRenderer(
anchor, anchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -431,7 +433,7 @@ function baseCreateRenderer(
anchor, anchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -443,7 +445,7 @@ function baseCreateRenderer(
anchor, anchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized, optimized,
internals internals
@ -456,7 +458,7 @@ function baseCreateRenderer(
anchor, anchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized, optimized,
internals internals
@ -509,7 +511,7 @@ function baseCreateRenderer(
n2: VNode, n2: VNode,
container: RendererElement, container: RendererElement,
anchor: RendererNode | null, anchor: RendererNode | null,
isSVG: boolean namespace: ElementNamespace
) => { ) => {
// static nodes are only present when used with compiler-dom/runtime-dom // static nodes are only present when used with compiler-dom/runtime-dom
// which guarantees presence of hostInsertStaticContent. // which guarantees presence of hostInsertStaticContent.
@ -517,7 +519,7 @@ function baseCreateRenderer(
n2.children as string, n2.children as string,
container, container,
anchor, anchor,
isSVG, namespace,
n2.el, n2.el,
n2.anchor n2.anchor
) )
@ -530,7 +532,7 @@ function baseCreateRenderer(
n1: VNode, n1: VNode,
n2: VNode, n2: VNode,
container: RendererElement, container: RendererElement,
isSVG: boolean namespace: ElementNamespace
) => { ) => {
// static nodes are only patched during dev for HMR // static nodes are only patched during dev for HMR
if (n2.children !== n1.children) { if (n2.children !== n1.children) {
@ -542,7 +544,7 @@ function baseCreateRenderer(
n2.children as string, n2.children as string,
container, container,
anchor, anchor,
isSVG namespace
) )
} else { } else {
n2.el = n1.el n2.el = n1.el
@ -581,11 +583,16 @@ function baseCreateRenderer(
anchor: RendererNode | null, anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null, parentSuspense: SuspenseBoundary | null,
isSVG: boolean, namespace: ElementNamespace,
slotScopeIds: string[] | null, slotScopeIds: string[] | null,
optimized: boolean optimized: boolean
) => { ) => {
isSVG = isSVG || n2.type === 'svg' if (n2.type === 'svg') {
namespace = 'svg'
} else if (n2.type === 'math') {
namespace = 'mathml'
}
if (n1 == null) { if (n1 == null) {
mountElement( mountElement(
n2, n2,
@ -593,7 +600,7 @@ function baseCreateRenderer(
anchor, anchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -603,7 +610,7 @@ function baseCreateRenderer(
n2, n2,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -616,17 +623,17 @@ function baseCreateRenderer(
anchor: RendererNode | null, anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null, parentSuspense: SuspenseBoundary | null,
isSVG: boolean, namespace: ElementNamespace,
slotScopeIds: string[] | null, slotScopeIds: string[] | null,
optimized: boolean optimized: boolean
) => { ) => {
let el: RendererElement let el: RendererElement
let vnodeHook: VNodeHook | undefined | null let vnodeHook: VNodeHook | undefined | null
const { type, props, shapeFlag, transition, dirs } = vnode const { props, shapeFlag, transition, dirs } = vnode
el = vnode.el = hostCreateElement( el = vnode.el = hostCreateElement(
vnode.type as string, vnode.type as string,
isSVG, namespace,
props && props.is, props && props.is,
props props
) )
@ -642,7 +649,7 @@ function baseCreateRenderer(
null, null,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG && type !== 'foreignObject', resolveChildrenNamespace(vnode, namespace),
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -662,7 +669,7 @@ function baseCreateRenderer(
key, key,
null, null,
props[key], props[key],
isSVG, namespace,
vnode.children as VNode[], vnode.children as VNode[],
parentComponent, parentComponent,
parentSuspense, parentSuspense,
@ -680,7 +687,7 @@ function baseCreateRenderer(
* affect non-DOM renderers) * affect non-DOM renderers)
*/ */
if ('value' in props) { if ('value' in props) {
hostPatchProp(el, 'value', null, props.value) hostPatchProp(el, 'value', null, props.value, namespace)
} }
if ((vnodeHook = props.onVnodeBeforeMount)) { if ((vnodeHook = props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parentComponent, vnode) invokeVNodeHook(vnodeHook, parentComponent, vnode)
@ -764,7 +771,7 @@ function baseCreateRenderer(
anchor, anchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace: ElementNamespace,
slotScopeIds, slotScopeIds,
optimized, optimized,
start = 0 start = 0
@ -780,7 +787,7 @@ function baseCreateRenderer(
anchor, anchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -792,7 +799,7 @@ function baseCreateRenderer(
n2: VNode, n2: VNode,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null, parentSuspense: SuspenseBoundary | null,
isSVG: boolean, namespace: ElementNamespace,
slotScopeIds: string[] | null, slotScopeIds: string[] | null,
optimized: boolean optimized: boolean
) => { ) => {
@ -822,7 +829,6 @@ function baseCreateRenderer(
dynamicChildren = null dynamicChildren = null
} }
const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
if (dynamicChildren) { if (dynamicChildren) {
patchBlockChildren( patchBlockChildren(
n1.dynamicChildren!, n1.dynamicChildren!,
@ -830,7 +836,7 @@ function baseCreateRenderer(
el, el,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
areChildrenSVG, resolveChildrenNamespace(n2, namespace),
slotScopeIds slotScopeIds
) )
if (__DEV__) { if (__DEV__) {
@ -846,7 +852,7 @@ function baseCreateRenderer(
null, null,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
areChildrenSVG, resolveChildrenNamespace(n2, namespace),
slotScopeIds, slotScopeIds,
false false
) )
@ -866,21 +872,21 @@ function baseCreateRenderer(
newProps, newProps,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG namespace
) )
} else { } else {
// class // class
// this flag is matched when the element has dynamic class bindings. // this flag is matched when the element has dynamic class bindings.
if (patchFlag & PatchFlags.CLASS) { if (patchFlag & PatchFlags.CLASS) {
if (oldProps.class !== newProps.class) { if (oldProps.class !== newProps.class) {
hostPatchProp(el, 'class', null, newProps.class, isSVG) hostPatchProp(el, 'class', null, newProps.class, namespace)
} }
} }
// style // style
// this flag is matched when the element has dynamic style bindings // this flag is matched when the element has dynamic style bindings
if (patchFlag & PatchFlags.STYLE) { if (patchFlag & PatchFlags.STYLE) {
hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG) hostPatchProp(el, 'style', oldProps.style, newProps.style, namespace)
} }
// props // props
@ -903,7 +909,7 @@ function baseCreateRenderer(
key, key,
prev, prev,
next, next,
isSVG, namespace,
n1.children as VNode[], n1.children as VNode[],
parentComponent, parentComponent,
parentSuspense, parentSuspense,
@ -930,7 +936,7 @@ function baseCreateRenderer(
newProps, newProps,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG namespace
) )
} }
@ -949,7 +955,7 @@ function baseCreateRenderer(
fallbackContainer, fallbackContainer,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace: ElementNamespace,
slotScopeIds slotScopeIds
) => { ) => {
for (let i = 0; i < newChildren.length; i++) { for (let i = 0; i < newChildren.length; i++) {
@ -979,7 +985,7 @@ function baseCreateRenderer(
null, null,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
true true
) )
@ -993,7 +999,7 @@ function baseCreateRenderer(
newProps: Data, newProps: Data,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null, parentSuspense: SuspenseBoundary | null,
isSVG: boolean namespace: ElementNamespace
) => { ) => {
if (oldProps !== newProps) { if (oldProps !== newProps) {
if (oldProps !== EMPTY_OBJ) { if (oldProps !== EMPTY_OBJ) {
@ -1004,7 +1010,7 @@ function baseCreateRenderer(
key, key,
oldProps[key], oldProps[key],
null, null,
isSVG, namespace,
vnode.children as VNode[], vnode.children as VNode[],
parentComponent, parentComponent,
parentSuspense, parentSuspense,
@ -1025,7 +1031,7 @@ function baseCreateRenderer(
key, key,
prev, prev,
next, next,
isSVG, namespace,
vnode.children as VNode[], vnode.children as VNode[],
parentComponent, parentComponent,
parentSuspense, parentSuspense,
@ -1034,7 +1040,7 @@ function baseCreateRenderer(
} }
} }
if ('value' in newProps) { 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, anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null, parentSuspense: SuspenseBoundary | null,
isSVG: boolean, namespace: ElementNamespace,
slotScopeIds: string[] | null, slotScopeIds: string[] | null,
optimized: boolean optimized: boolean
) => { ) => {
@ -1085,7 +1091,7 @@ function baseCreateRenderer(
fragmentEndAnchor, fragmentEndAnchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -1106,7 +1112,7 @@ function baseCreateRenderer(
container, container,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds slotScopeIds
) )
if (__DEV__) { if (__DEV__) {
@ -1134,7 +1140,7 @@ function baseCreateRenderer(
fragmentEndAnchor, fragmentEndAnchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -1149,7 +1155,7 @@ function baseCreateRenderer(
anchor: RendererNode | null, anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null, parentSuspense: SuspenseBoundary | null,
isSVG: boolean, namespace: ElementNamespace,
slotScopeIds: string[] | null, slotScopeIds: string[] | null,
optimized: boolean optimized: boolean
) => { ) => {
@ -1160,7 +1166,7 @@ function baseCreateRenderer(
n2, n2,
container, container,
anchor, anchor,
isSVG, namespace,
optimized optimized
) )
} else { } else {
@ -1170,7 +1176,7 @@ function baseCreateRenderer(
anchor, anchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
optimized optimized
) )
} }
@ -1185,7 +1191,7 @@ function baseCreateRenderer(
anchor, anchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace: ElementNamespace,
optimized optimized
) => { ) => {
// 2.x compat may pre-create the component instance before actually // 2.x compat may pre-create the component instance before actually
@ -1245,7 +1251,7 @@ function baseCreateRenderer(
container, container,
anchor, anchor,
parentSuspense, parentSuspense,
isSVG, namespace,
optimized optimized
) )
@ -1296,7 +1302,7 @@ function baseCreateRenderer(
container, container,
anchor, anchor,
parentSuspense, parentSuspense,
isSVG, namespace: ElementNamespace,
optimized optimized
) => { ) => {
const componentUpdateFn = () => { const componentUpdateFn = () => {
@ -1380,7 +1386,7 @@ function baseCreateRenderer(
anchor, anchor,
instance, instance,
parentSuspense, parentSuspense,
isSVG namespace
) )
if (__DEV__) { if (__DEV__) {
endMeasure(instance, `patch`) endMeasure(instance, `patch`)
@ -1499,7 +1505,7 @@ function baseCreateRenderer(
getNextHostNode(prevTree), getNextHostNode(prevTree),
instance, instance,
parentSuspense, parentSuspense,
isSVG namespace
) )
if (__DEV__) { if (__DEV__) {
endMeasure(instance, `patch`) endMeasure(instance, `patch`)
@ -1599,7 +1605,7 @@ function baseCreateRenderer(
anchor, anchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace: ElementNamespace,
slotScopeIds, slotScopeIds,
optimized = false optimized = false
) => { ) => {
@ -1620,7 +1626,7 @@ function baseCreateRenderer(
anchor, anchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -1634,7 +1640,7 @@ function baseCreateRenderer(
anchor, anchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -1663,7 +1669,7 @@ function baseCreateRenderer(
anchor, anchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -1685,7 +1691,7 @@ function baseCreateRenderer(
anchor, anchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -1701,7 +1707,7 @@ function baseCreateRenderer(
anchor: RendererNode | null, anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null, parentSuspense: SuspenseBoundary | null,
isSVG: boolean, namespace: ElementNamespace,
slotScopeIds: string[] | null, slotScopeIds: string[] | null,
optimized: boolean optimized: boolean
) => { ) => {
@ -1722,7 +1728,7 @@ function baseCreateRenderer(
null, null,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -1745,7 +1751,7 @@ function baseCreateRenderer(
anchor, anchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized, optimized,
commonLength commonLength
@ -1761,7 +1767,7 @@ function baseCreateRenderer(
parentAnchor: RendererNode | null, parentAnchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null, parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null, parentSuspense: SuspenseBoundary | null,
isSVG: boolean, namespace: ElementNamespace,
slotScopeIds: string[] | null, slotScopeIds: string[] | null,
optimized: boolean optimized: boolean
) => { ) => {
@ -1786,7 +1792,7 @@ function baseCreateRenderer(
null, null,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -1812,7 +1818,7 @@ function baseCreateRenderer(
null, null,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -1844,7 +1850,7 @@ function baseCreateRenderer(
anchor, anchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -1947,7 +1953,7 @@ function baseCreateRenderer(
null, null,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -1976,7 +1982,7 @@ function baseCreateRenderer(
anchor, anchor,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
isSVG, namespace,
slotScopeIds, slotScopeIds,
optimized optimized
) )
@ -2321,13 +2327,21 @@ function baseCreateRenderer(
return hostNextSibling((vnode.anchor || vnode.el)!) return hostNextSibling((vnode.anchor || vnode.el)!)
} }
const render: RootRenderFunction = (vnode, container, isSVG) => { const render: RootRenderFunction = (vnode, container, namespace) => {
if (vnode == null) { if (vnode == null) {
if (container._vnode) { if (container._vnode) {
unmount(container._vnode, null, null, true) unmount(container._vnode, null, null, true)
} }
} else { } else {
patch(container._vnode || null, vnode, container, null, null, null, isSVG) patch(
container._vnode || null,
vnode,
container,
null,
null,
null,
namespace
)
} }
flushPreFlushCbs() flushPreFlushCbs()
flushPostFlushCbs() 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( function toggleRecurse(
{ effect, update }: ComponentInternalInstance, { effect, update }: ComponentInternalInstance,
allowed: boolean allowed: boolean

View File

@ -2,7 +2,7 @@ import { nodeOps, svgNS } from '../src/nodeOps'
describe('runtime-dom: node-ops', () => { describe('runtime-dom: node-ops', () => {
test("the <select>'s multiple attr should be set in createElement", () => { 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: '' multiple: ''
}) as HTMLSelectElement }) as HTMLSelectElement
const option1 = nodeOps.createElement('option') as HTMLOptionElement const option1 = nodeOps.createElement('option') as HTMLOptionElement
@ -21,7 +21,12 @@ describe('runtime-dom: node-ops', () => {
test('fresh insertion', () => { test('fresh insertion', () => {
const content = `<div>one</div><div>two</div>three` const content = `<div>one</div><div>two</div>three`
const parent = document.createElement('div') 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(parent.innerHTML).toBe(content)
expect(nodes[0]).toBe(parent.firstChild) expect(nodes[0]).toBe(parent.firstChild)
expect(nodes[1]).toBe(parent.lastChild) expect(nodes[1]).toBe(parent.lastChild)
@ -33,7 +38,12 @@ describe('runtime-dom: node-ops', () => {
const parent = document.createElement('div') const parent = document.createElement('div')
parent.innerHTML = existing parent.innerHTML = existing
const anchor = parent.firstChild 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(parent.innerHTML).toBe(content + existing)
expect(nodes[0]).toBe(parent.firstChild) expect(nodes[0]).toBe(parent.firstChild)
expect(nodes[1]).toBe(parent.childNodes[parent.childNodes.length - 2]) expect(nodes[1]).toBe(parent.childNodes[parent.childNodes.length - 2])
@ -46,7 +56,7 @@ describe('runtime-dom: node-ops', () => {
content, content,
parent, parent,
null, null,
true 'svg'
) )
expect(parent.innerHTML).toBe(content) expect(parent.innerHTML).toBe(content)
expect(first).toBe(parent.firstChild) expect(first).toBe(parent.firstChild)
@ -65,7 +75,7 @@ describe('runtime-dom: node-ops', () => {
content, content,
parent, parent,
anchor, anchor,
true 'svg'
) )
expect(parent.innerHTML).toBe(content + existing) expect(parent.innerHTML).toBe(content + existing)
expect(first).toBe(parent.firstChild) expect(first).toBe(parent.firstChild)
@ -88,7 +98,7 @@ describe('runtime-dom: node-ops', () => {
content, content,
parent, parent,
anchor, anchor,
false, undefined,
cached.firstChild, cached.firstChild,
cached.lastChild cached.lastChild
) )

View File

@ -4,15 +4,15 @@ import { xlinkNS } from '../src/modules/attrs'
describe('runtime-dom: attrs patching', () => { describe('runtime-dom: attrs patching', () => {
test('xlink attributes', () => { test('xlink attributes', () => {
const el = document.createElementNS('http://www.w3.org/2000/svg', 'use') 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') 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) expect(el.getAttributeNS(xlinkNS, 'href')).toBe(null)
}) })
test('textContent attributes /w svg', () => { test('textContent attributes /w svg', () => {
const el = document.createElementNS('http://www.w3.org/2000/svg', 'use') 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.attributes.length).toBe(0)
expect(el.innerHTML).toBe('foo') expect(el.innerHTML).toBe('foo')
}) })

View File

@ -25,7 +25,7 @@ describe('runtime-dom: class patching', () => {
test('svg', () => { test('svg', () => {
const el = document.createElementNS(svgNS, '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') expect(el.getAttribute('class')).toBe('foo')
}) })
}) })

View File

@ -10,7 +10,8 @@ import {
RootHydrateFunction, RootHydrateFunction,
isRuntimeOnly, isRuntimeOnly,
DeprecationTypes, DeprecationTypes,
compatUtils compatUtils,
ElementNamespace
} from '@vue/runtime-core' } from '@vue/runtime-core'
import { nodeOps } from './nodeOps' import { nodeOps } from './nodeOps'
import { patchProp } from './patchProp' import { patchProp } from './patchProp'
@ -21,7 +22,8 @@ import {
isHTMLTag, isHTMLTag,
isSVGTag, isSVGTag,
extend, extend,
NOOP NOOP,
isMathMLTag
} from '@vue/shared' } from '@vue/shared'
declare module '@vue/reactivity' { declare module '@vue/reactivity' {
@ -99,7 +101,7 @@ export const createApp = ((...args) => {
// clear content before mounting // clear content before mounting
container.innerHTML = '' container.innerHTML = ''
const proxy = mount(container, false, container instanceof SVGElement) const proxy = mount(container, false, resolveRootNamespace(container))
if (container instanceof Element) { if (container instanceof Element) {
container.removeAttribute('v-cloak') container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '') container.setAttribute('data-v-app', '')
@ -122,18 +124,30 @@ export const createSSRApp = ((...args) => {
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => { app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
const container = normalizeContainer(containerOrSelector) const container = normalizeContainer(containerOrSelector)
if (container) { if (container) {
return mount(container, true, container instanceof SVGElement) return mount(container, true, resolveRootNamespace(container))
} }
} }
return app return app
}) as CreateAppFunction<Element> }) 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) { function injectNativeTagCheck(app: App) {
// Inject `isNativeTag` // Inject `isNativeTag`
// this is used for component name validation (dev only) // this is used for component name validation (dev only)
Object.defineProperty(app.config, 'isNativeTag', { Object.defineProperty(app.config, 'isNativeTag', {
value: (tag: string) => isHTMLTag(tag) || isSVGTag(tag), value: (tag: string) => isHTMLTag(tag) || isSVGTag(tag) || isMathMLTag(tag),
writable: false writable: false
}) })
} }

View File

@ -1,6 +1,7 @@
import { RendererOptions } from '@vue/runtime-core' import { RendererOptions } from '@vue/runtime-core'
export const svgNS = 'http://www.w3.org/2000/svg' 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 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 => { createElement: (tag, namespace, is, props): Element => {
const el = isSVG const el =
? doc.createElementNS(svgNS, tag) namespace === 'svg'
: doc.createElement(tag, is ? { is } : undefined) ? doc.createElementNS(svgNS, tag)
: namespace === 'mathml'
? doc.createElementNS(mathmlNS, tag)
: doc.createElement(tag, is ? { is } : undefined)
if (tag === 'select' && props && props.multiple != null) { if (tag === 'select' && props && props.multiple != null) {
;(el as HTMLSelectElement).setAttribute('multiple', props.multiple) ;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
@ -56,7 +60,7 @@ export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
// Reason: innerHTML. // Reason: innerHTML.
// Static content here can only come from compiled templates. // Static content here can only come from compiled templates.
// As long as the user only uses trusted templates, this is safe. // 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> // <parent> before | first ... last | anchor </parent>
const before = anchor ? anchor.previousSibling : parent.lastChild const before = anchor ? anchor.previousSibling : parent.lastChild
// #5308 can only take cached path if: // #5308 can only take cached path if:
@ -70,10 +74,16 @@ export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
} }
} else { } else {
// fresh insert // 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 const template = templateContainer.content
if (isSVG) { if (namespace === 'svg' || namespace === 'mathml') {
// remove outer svg wrapper // remove outer svg/math wrapper
const wrapper = template.firstChild! const wrapper = template.firstChild!
while (wrapper.firstChild) { while (wrapper.firstChild) {
template.appendChild(wrapper.firstChild) template.appendChild(wrapper.firstChild)

View File

@ -20,12 +20,13 @@ export const patchProp: DOMRendererOptions['patchProp'] = (
key, key,
prevValue, prevValue,
nextValue, nextValue,
isSVG = false, namespace,
prevChildren, prevChildren,
parentComponent, parentComponent,
parentSuspense, parentSuspense,
unmountChildren unmountChildren
) => { ) => {
const isSVG = namespace === 'svg'
if (key === 'class') { if (key === 'class') {
patchClass(el, nextValue, isSVG) patchClass(el, nextValue, isSVG)
} else if (key === 'style') { } else if (key === 'style') {

View File

@ -27,6 +27,13 @@ const SVG_TAGS =
'polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,' + 'polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,' +
'text,textPath,title,tspan,unknown,use,view' '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 = const VOID_TAGS =
'area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr' '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. * Do NOT use in runtime code paths unless behind `__DEV__` flag.
*/ */
export const isSVGTag = /*#__PURE__*/ makeMap(SVG_TAGS) 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. * Compiler only.
* Do NOT use in runtime code paths unless behind `__DEV__` flag. * Do NOT use in runtime code paths unless behind `__DEV__` flag.

View File

@ -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')
})
})

View File

@ -9,7 +9,11 @@ import { vtcKey } from '../../runtime-dom/src/components/Transition'
import { render, h, ref, nextTick } from '../src' import { render, h, ref, nextTick } from '../src'
describe('SVG support', () => { 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') const root = document.createElement('div')
document.body.appendChild(root) document.body.appendChild(root)
const App = { const App = {
@ -18,6 +22,8 @@ describe('SVG support', () => {
<svg id="e1"> <svg id="e1">
<foreignObject id="e2"> <foreignObject id="e2">
<div id="e3"/> <div id="e3"/>
<svg id="e4"/>
<math id="e5"/>
</foreignObject> </foreignObject>
</svg> </svg>
</div> </div>
@ -29,6 +35,8 @@ describe('SVG support', () => {
expect(e0.querySelector('#e1')!.namespaceURI).toMatch('svg') expect(e0.querySelector('#e1')!.namespaceURI).toMatch('svg')
expect(e0.querySelector('#e2')!.namespaceURI).toMatch('svg') expect(e0.querySelector('#e2')!.namespaceURI).toMatch('svg')
expect(e0.querySelector('#e3')!.namespaceURI).toMatch('xhtml') 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 () => { test('should patch elements with correct namespaces', async () => {

View File

@ -1,5 +1,7 @@
import { type SpyInstance } from 'vitest' import { type SpyInstance } from 'vitest'
vi.stubGlobal('MathMLElement', class MathMLElement {})
expect.extend({ expect.extend({
toHaveBeenWarned(received: string) { toHaveBeenWarned(received: string) {
asserted.add(received) asserted.add(received)