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,
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'
mount(
rootContainer: HostElement | string,
isHydrate?: boolean,
- isSVG?: boolean
+ namespace?: boolean | ElementNamespace
): ComponentPublicInstance
unmount(): void
provide<T>(key: InjectionKey<T> | string, value: T): this
mount(
rootContainer: HostElement,
isHydrate?: boolean,
- isSVG?: boolean
+ namespace?: boolean | ElementNamespace
): any {
if (!isMounted) {
// #5571
// 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
} from '@vue/shared'
import { warn } from '../warning'
import { cloneVNode, createVNode } from '../vnode'
-import { RootRenderFunction } from '../renderer'
+import { ElementNamespace, RootRenderFunction } from '../renderer'
import {
App,
AppConfig,
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__) {
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)
}
}
container.innerHTML = ''
// TODO hydration
- render(vnode, container, isSVG)
+ render(vnode, container, namespace)
if (container instanceof Element) {
container.removeAttribute('v-cloak')
queuePostRenderEffect,
MoveType,
RendererElement,
- RendererNode
+ RendererNode,
+ ElementNamespace
} from '../renderer'
import { setTransitionHooks } from './BaseTransition'
import { ComponentRenderContext } from '../componentPublicInstance'
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
- isSVG: boolean,
+ namespace: ElementNamespace,
optimized: boolean
) => void
deactivate: (vnode: VNode) => void
} = 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
anchor,
instance,
parentSuspense,
- isSVG,
+ namespace,
vnode.slotScopeIds,
optimized
)
MoveType,
SetupRenderEffectFn,
RendererNode,
- RendererElement
+ RendererElement,
+ ElementNamespace
} from '../renderer'
import { queuePostFlushCb } from '../scheduler'
import { filterSingleRoot, updateHOCHostEl } from '../componentRenderUtils'
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
- isSVG: boolean,
+ namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
// platform-specific impl passed from renderer
anchor,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized,
rendererInternals
container,
anchor,
parentComponent,
- isSVG,
+ namespace,
slotScopeIds,
optimized,
rendererInternals
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
- isSVG: boolean,
+ namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
rendererInternals: RendererInternals
container,
hiddenContainer,
anchor,
- isSVG,
+ namespace,
slotScopeIds,
optimized,
rendererInternals
null,
parentComponent,
suspense,
- isSVG,
+ namespace,
slotScopeIds
)
// now check if we have encountered any async deps
anchor,
parentComponent,
null, // fallback tree will not have suspense context
- isSVG,
+ namespace,
slotScopeIds
)
setActiveBranch(suspense, vnode.ssFallback!)
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
- isSVG: boolean,
+ namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
{ p: patch, um: unmount, o: { createElement } }: RendererInternals
null,
parentComponent,
suspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
anchor,
parentComponent,
null, // fallback tree will not have suspense context
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
null,
parentComponent,
suspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
anchor,
parentComponent,
null, // fallback tree will not have suspense context
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
anchor,
parentComponent,
suspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
null,
parentComponent,
suspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
anchor,
parentComponent,
suspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
null,
parentComponent,
suspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
vnode: VNode<RendererNode, RendererElement, SuspenseProps>
parent: SuspenseBoundary | null
parentComponent: ComponentInternalInstance | null
- isSVG: boolean
+ namespace: ElementNamespace
container: RendererElement
hiddenContainer: RendererElement
anchor: RendererNode | null
container: RendererElement,
hiddenContainer: RendererElement,
anchor: RendererNode | null,
- isSVG: boolean,
+ namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
rendererInternals: RendererInternals,
vnode,
parent: parentSuspense,
parentComponent,
- isSVG,
+ namespace,
container,
hiddenContainer,
anchor,
return
}
- const { vnode, activeBranch, parentComponent, container, isSVG } =
+ const { vnode, activeBranch, parentComponent, container, namespace } =
suspense
// invoke @fallback event
next(activeBranch!),
parentComponent,
null, // fallback tree will not have suspense context
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
// consider the comment placeholder case.
hydratedEl ? null : next(instance.subTree),
suspense,
- isSVG,
+ namespace,
optimized
)
if (placeholder) {
vnode: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
- isSVG: boolean,
+ namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
rendererInternals: RendererInternals,
node.parentNode!,
document.createElement('div'),
null,
- isSVG,
+ namespace,
slotScopeIds,
optimized,
rendererInternals,
RendererElement,
RendererNode,
RendererOptions,
- traverseStaticChildren
+ traverseStaticChildren,
+ ElementNamespace
} from '../renderer'
import { VNode, VNodeArrayChildren, VNodeProps } from '../vnode'
import { isString, ShapeFlags } from '@vue/shared'
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']
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
- isSVG: boolean,
+ namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
internals: RendererInternals
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})`)
}
anchor,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
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
currentContainer,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds
)
// even in block tree mode we need to make sure all root-level nodes
currentAnchor,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
false
)
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
null,
parentComponent,
parentSuspense,
- isSVGContainer(container),
+ getContainerType(container),
optimized
)
vnode,
parentComponent,
parentSuspense,
- isSVGContainer(parentNode(node)!),
+ getContainerType(parentNode(node)!),
slotScopeIds,
optimized,
rendererInternals,
key,
null,
props[key],
- false,
+ undefined,
undefined,
parentComponent
)
'onClick',
null,
props.onClick,
- false,
+ undefined,
undefined,
parentComponent
)
null,
parentComponent,
parentSuspense,
- isSVGContainer(container),
+ getContainerType(container),
slotScopeIds
)
}
next,
parentComponent,
parentSuspense,
- isSVGContainer(container),
+ getContainerType(container),
slotScopeIds
)
return next
RendererElement,
HydrationRenderer,
RendererOptions,
- RootRenderFunction
+ RootRenderFunction,
+ ElementNamespace
} from './renderer'
export type { RootHydrateFunction } from './hydration'
export type { Slot, Slots, SlotsType } from './componentSlots'
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<
key: string,
prevValue: any,
nextValue: any,
- isSVG?: boolean,
+ namespace?: ElementNamespace,
prevChildren?: VNode<HostNode, HostElement>[],
parentComponent?: ComponentInternalInstance | null,
parentSuspense?: SuspenseBoundary | null,
remove(el: HostNode): void
createElement(
type: string,
- isSVG?: boolean,
+ namespace?: ElementNamespace,
isCustomizedBuiltIn?: string,
vnodeProps?: (VNodeProps & { [key: string]: any }) | null
): HostElement
content: string,
parent: HostElement,
anchor: HostNode | null,
- isSVG: boolean,
+ namespace: ElementNamespace,
start?: HostNode | null,
end?: HostNode | null
): [HostNode, HostNode]
anchor?: RendererNode | null,
parentComponent?: ComponentInternalInstance | null,
parentSuspense?: SuspenseBoundary | null,
- isSVG?: boolean,
+ namespace?: ElementNamespace,
slotScopeIds?: string[] | null,
optimized?: boolean
) => void
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
- isSVG: boolean,
+ namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
start?: number
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
- isSVG: boolean,
+ namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean
) => void
fallbackContainer: RendererElement,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
- isSVG: boolean,
+ namespace: ElementNamespace,
slotScopeIds: string[] | null
) => void
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
- isSVG: boolean,
+ namespace: ElementNamespace,
optimized: boolean
) => void
container: RendererElement,
anchor: RendererNode | null,
parentSuspense: SuspenseBoundary | null,
- isSVG: boolean,
+ namespace: ElementNamespace,
optimized: boolean
) => void
anchor = null,
parentComponent = null,
parentSuspense = null,
- isSVG = false,
+ namespace = undefined,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
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:
anchor,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
anchor,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
anchor,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
anchor,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized,
internals
anchor,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized,
internals
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.
n2.children as string,
container,
anchor,
- isSVG,
+ namespace,
n2.el,
n2.anchor
)
n1: VNode,
n2: VNode,
container: RendererElement,
- isSVG: boolean
+ namespace: ElementNamespace
) => {
// static nodes are only patched during dev for HMR
if (n2.children !== n1.children) {
n2.children as string,
container,
anchor,
- isSVG
+ namespace
)
} else {
n2.el = n1.el
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,
anchor,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
n2,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
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
)
null,
parentComponent,
parentSuspense,
- isSVG && type !== 'foreignObject',
+ resolveChildrenNamespace(vnode, namespace),
slotScopeIds,
optimized
)
key,
null,
props[key],
- isSVG,
+ namespace,
vnode.children as VNode[],
parentComponent,
parentSuspense,
* 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)
anchor,
parentComponent,
parentSuspense,
- isSVG,
+ namespace: ElementNamespace,
slotScopeIds,
optimized,
start = 0
anchor,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
n2: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
- isSVG: boolean,
+ namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean
) => {
dynamicChildren = null
}
- const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
if (dynamicChildren) {
patchBlockChildren(
n1.dynamicChildren!,
el,
parentComponent,
parentSuspense,
- areChildrenSVG,
+ resolveChildrenNamespace(n2, namespace),
slotScopeIds
)
if (__DEV__) {
null,
parentComponent,
parentSuspense,
- areChildrenSVG,
+ resolveChildrenNamespace(n2, namespace),
slotScopeIds,
false
)
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
key,
prev,
next,
- isSVG,
+ namespace,
n1.children as VNode[],
parentComponent,
parentSuspense,
newProps,
parentComponent,
parentSuspense,
- isSVG
+ namespace
)
}
fallbackContainer,
parentComponent,
parentSuspense,
- isSVG,
+ namespace: ElementNamespace,
slotScopeIds
) => {
for (let i = 0; i < newChildren.length; i++) {
null,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
true
)
newProps: Data,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
- isSVG: boolean
+ namespace: ElementNamespace
) => {
if (oldProps !== newProps) {
if (oldProps !== EMPTY_OBJ) {
key,
oldProps[key],
null,
- isSVG,
+ namespace,
vnode.children as VNode[],
parentComponent,
parentSuspense,
key,
prev,
next,
- isSVG,
+ namespace,
vnode.children as VNode[],
parentComponent,
parentSuspense,
}
}
if ('value' in newProps) {
- hostPatchProp(el, 'value', oldProps.value, newProps.value)
+ hostPatchProp(el, 'value', oldProps.value, newProps.value, namespace)
}
}
}
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
- isSVG: boolean,
+ namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean
) => {
fragmentEndAnchor,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
container,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds
)
if (__DEV__) {
fragmentEndAnchor,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
- isSVG: boolean,
+ namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean
) => {
n2,
container,
anchor,
- isSVG,
+ namespace,
optimized
)
} else {
anchor,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
optimized
)
}
anchor,
parentComponent,
parentSuspense,
- isSVG,
+ namespace: ElementNamespace,
optimized
) => {
// 2.x compat may pre-create the component instance before actually
container,
anchor,
parentSuspense,
- isSVG,
+ namespace,
optimized
)
container,
anchor,
parentSuspense,
- isSVG,
+ namespace: ElementNamespace,
optimized
) => {
const componentUpdateFn = () => {
anchor,
instance,
parentSuspense,
- isSVG
+ namespace
)
if (__DEV__) {
endMeasure(instance, `patch`)
getNextHostNode(prevTree),
instance,
parentSuspense,
- isSVG
+ namespace
)
if (__DEV__) {
endMeasure(instance, `patch`)
anchor,
parentComponent,
parentSuspense,
- isSVG,
+ namespace: ElementNamespace,
slotScopeIds,
optimized = false
) => {
anchor,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
anchor,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
anchor,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
anchor,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
- isSVG: boolean,
+ namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean
) => {
null,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
anchor,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized,
commonLength
parentAnchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
- isSVG: boolean,
+ namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean
) => {
null,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
null,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
anchor,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
null,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
anchor,
parentComponent,
parentSuspense,
- isSVG,
+ namespace,
slotScopeIds,
optimized
)
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()
}
}
+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
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
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)
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])
content,
parent,
null,
- true
+ 'svg'
)
expect(parent.innerHTML).toBe(content)
expect(first).toBe(parent.firstChild)
content,
parent,
anchor,
- true
+ 'svg'
)
expect(parent.innerHTML).toBe(content + existing)
expect(first).toBe(parent.firstChild)
content,
parent,
anchor,
- false,
+ undefined,
cached.firstChild,
cached.lastChild
)
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')
})
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')
})
})
RootHydrateFunction,
isRuntimeOnly,
DeprecationTypes,
- compatUtils
+ compatUtils,
+ ElementNamespace
} from '@vue/runtime-core'
import { nodeOps } from './nodeOps'
import { patchProp } from './patchProp'
isHTMLTag,
isSVGTag,
extend,
- NOOP
+ NOOP,
+ isMathMLTag
} from '@vue/shared'
declare module '@vue/reactivity' {
// 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', '')
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
})
}
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
}
},
- 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)
// 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:
}
} 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)
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') {
'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'
* 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.
--- /dev/null
+// 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')
+ })
+})
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 = {
<svg id="e1">
<foreignObject id="e2">
<div id="e3"/>
+ <svg id="e4"/>
+ <math id="e5"/>
</foreignObject>
</svg>
</div>
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 () => {
import { type SpyInstance } from 'vitest'
+vi.stubGlobal('MathMLElement', class MathMLElement {})
+
expect.extend({
toHaveBeenWarned(received: string) {
asserted.add(received)