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 { 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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@ describe('runtime-dom: class patching', () => {
test('svg', () => {
const el = document.createElementNS(svgNS, 'svg')
patchProp(el, 'class', null, 'foo', true)
patchProp(el, 'class', null, 'foo', 'svg')
expect(el.getAttribute('class')).toBe('foo')
})
})

View File

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

View File

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

View File

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

View File

@ -27,6 +27,13 @@ const SVG_TAGS =
'polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,' +
'text,textPath,title,tspan,unknown,use,view'
// https://developer.mozilla.org/en-US/docs/Web/MathML/Element
const MATH_TAGS =
'math,maction,annotation,annotation-xml,menclose,merror,mfenced,mfrac,mi,' +
'mmultiscripts,mn,mo,mover,mpadded,mphantom,mprescripts,mroot,mrow,ms,' +
'semantics,mspace,msqrt,mstyle,msub,msup,msubsup,mtable,mtd,mtext,mtr,' +
'munder,munderover'
const VOID_TAGS =
'area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr'
@ -40,6 +47,11 @@ export const isHTMLTag = /*#__PURE__*/ makeMap(HTML_TAGS)
* Do NOT use in runtime code paths unless behind `__DEV__` flag.
*/
export const isSVGTag = /*#__PURE__*/ makeMap(SVG_TAGS)
/**
* Compiler only.
* Do NOT use in runtime code paths unless behind `__DEV__` flag.
*/
export const isMathMLTag = /*#__PURE__*/ makeMap(MATH_TAGS)
/**
* Compiler only.
* Do NOT use in runtime code paths unless behind `__DEV__` flag.

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'
describe('SVG support', () => {
test('should mount elements with correct namespaces', () => {
afterEach(() => {
document.body.innerHTML = ''
})
test('should mount elements with correct html namespace', () => {
const root = document.createElement('div')
document.body.appendChild(root)
const App = {
@ -18,6 +22,8 @@ describe('SVG support', () => {
<svg id="e1">
<foreignObject id="e2">
<div id="e3"/>
<svg id="e4"/>
<math id="e5"/>
</foreignObject>
</svg>
</div>
@ -29,6 +35,8 @@ describe('SVG support', () => {
expect(e0.querySelector('#e1')!.namespaceURI).toMatch('svg')
expect(e0.querySelector('#e2')!.namespaceURI).toMatch('svg')
expect(e0.querySelector('#e3')!.namespaceURI).toMatch('xhtml')
expect(e0.querySelector('#e4')!.namespaceURI).toMatch('svg')
expect(e0.querySelector('#e5')!.namespaceURI).toMatch('Math')
})
test('should patch elements with correct namespaces', async () => {

View File

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