type ElementNode,
ElementTypes,
type InterpolationNode,
- Namespaces,
NodeTypes,
type Position,
type TextNode,
import { baseParse } from '../src/parser'
import type { Program } from '@babel/types'
+import { Namespaces } from '@vue/shared'
describe('compiler: parse', () => {
describe('Text', () => {
import {
type ElementNode,
ElementTypes,
- Namespaces,
NodeTypes,
type Property,
type SimpleExpressionNode,
locStub,
} from '../src'
import {
+ Namespaces,
PatchFlagNames,
type PatchFlags,
type ShapeFlags,
-import { type PatchFlags, isString } from '@vue/shared'
+import { type Namespace, type PatchFlags, isString } from '@vue/shared'
import {
CREATE_BLOCK,
CREATE_ELEMENT_BLOCK,
import type { ImportItem, TransformContext } from './transform'
import type { Node as BabelNode } from '@babel/types'
-// Vue template is a platform-agnostic superset of HTML (syntax only).
-// More namespaces can be declared by platform specific compilers.
-export type Namespace = number
-
-export enum Namespaces {
- HTML,
- SVG,
- MATH_ML,
-}
-
export enum NodeTypes {
ROOT,
ELEMENT,
-import type {
- ElementNode,
- Namespace,
- Namespaces,
- ParentNode,
- TemplateChildNode,
-} from './ast'
+import type { ElementNode, ParentNode, TemplateChildNode } from './ast'
+import type { Namespace, Namespaces } from '@vue/shared'
import type { CompilerError } from './errors'
import type {
DirectiveTransform,
type ElementNode,
ElementTypes,
type ForParseResult,
- Namespaces,
NodeTypes,
type RootNode,
type SimpleExpressionNode,
createRoot,
createSimpleExpression,
} from './ast'
+import { Namespaces } from '@vue/shared'
import type { ParserOptions } from './options'
import Tokenizer, {
CharCodes,
type ElementNode,
ElementTypes,
type InterpolationNode,
- Namespaces,
NodeTypes,
type TextNode,
baseParse as parse,
} from '@vue/compiler-core'
import { parserOptions } from '../src/parserOptions'
+import { Namespaces } from '@vue/shared'
describe('DOM parser', () => {
describe('Text', () => {
expect(element.ns).toBe(Namespaces.SVG)
})
+ test('SVG tags without explicit root', () => {
+ const ast = parse('<text/><view/><tspan/>', parserOptions)
+ const textNode = ast.children[0] as ElementNode
+ const viewNode = ast.children[1] as ElementNode
+ const tspanNode = ast.children[2] as ElementNode
+
+ expect(textNode.ns).toBe(Namespaces.SVG)
+ expect(viewNode.ns).toBe(Namespaces.SVG)
+ expect(tspanNode.ns).toBe(Namespaces.SVG)
+ })
+
test('MATH in HTML namespace', () => {
const ast = parse('<html><math></math></html>', parserOptions)
const elementHtml = ast.children[0] as ElementNode
expect(element.ns).toBe(Namespaces.MATH_ML)
})
+ test('MATH tags without explicit root', () => {
+ const ast = parse('<mi/><mn/><mo/>', parserOptions)
+ const miNode = ast.children[0] as ElementNode
+ const mnNode = ast.children[1] as ElementNode
+ const moNode = ast.children[2] as ElementNode
+
+ expect(miNode.ns).toBe(Namespaces.MATH_ML)
+ expect(mnNode.ns).toBe(Namespaces.MATH_ML)
+ expect(moNode.ns).toBe(Namespaces.MATH_ML)
+ })
+
test('root ns', () => {
const ast = parse('<foreignObject><test/></foreignObject>', {
...parserOptions,
-import { Namespaces, NodeTypes, type ParserOptions } from '@vue/compiler-core'
-import { isHTMLTag, isMathMLTag, isSVGTag, isVoidTag } from '@vue/shared'
+import { NodeTypes, type ParserOptions } from '@vue/compiler-core'
+import {
+ Namespaces,
+ isHTMLTag,
+ isMathMLTag,
+ isSVGTag,
+ isVoidTag,
+} from '@vue/shared'
import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
import { decodeHtmlBrowser } from './decodeHtmlBrowser'
let ns = parent ? parent.ns : rootNamespace
if (parent && ns === Namespaces.MATH_ML) {
if (parent.tag === 'annotation-xml') {
- if (tag === 'svg') {
+ if (isSVGTag(tag)) {
return Namespaces.SVG
}
if (
}
if (ns === Namespaces.HTML) {
- if (tag === 'svg') {
+ if (isSVGTag(tag)) {
return Namespaces.SVG
}
- if (tag === 'math') {
+ if (isMathMLTag(tag)) {
return Namespaces.MATH_ML
}
}
ElementTypes,
type ExpressionNode,
type HoistTransform,
- Namespaces,
NodeTypes,
type PlainElementNode,
type SimpleExpressionNode,
isStaticArgOf,
} from '@vue/compiler-core'
import {
+ Namespaces,
escapeHtml,
isArray,
isBooleanAttr,
type ExpressionNode,
type FunctionExpression,
type JSChildNode,
- Namespaces,
type NodeTransform,
NodeTypes,
RESOLVE_DYNAMIC_COMPONENT,
ssrProcessTransitionGroup,
ssrTransformTransitionGroup,
} from './ssrTransformTransitionGroup'
-import { extend, isArray, isObject, isPlainObject, isSymbol } from '@vue/shared'
+import {
+ Namespaces,
+ extend,
+ isArray,
+ isObject,
+ isPlainObject,
+ isSymbol,
+} from '@vue/shared'
import { buildSSRProps } from './ssrTransformElement'
import {
ssrProcessTransition,
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+exports[`compiler: element transform > MathML 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<math><mrow><mi>x</mi></mrow></math>", true, 2)
+
+export function render(_ctx) {
+ const n0 = t0()
+ return n0
+}"
+`;
+
exports[`compiler: element transform > checkbox with static indeterminate 1`] = `
"import { setProp as _setProp, template as _template } from 'vue';
const t0 = _template("<input type=\\"checkbox\\">", true)
}"
`;
+exports[`compiler: element transform > svg 1`] = `
+"import { template as _template } from 'vue';
+const t0 = _template("<svg><circle r=\\"40\\"></circle></svg>", true, 1)
+
+export function render(_ctx) {
+ const n0 = t0()
+ return n0
+}"
+`;
+
exports[`compiler: element transform > v-bind="obj" 1`] = `
"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("<div></div>", true)
}"
`;
+exports[`compiler v-bind > :class w/ svg elements 1`] = `
+"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<svg></svg>", true, 1)
+
+export function render(_ctx) {
+ const n0 = t0()
+ _renderEffect(() => _setAttr(n0, "class", _ctx.cls, true))
+ return n0
+}"
+`;
+
exports[`compiler v-bind > :innerHTML 1`] = `
"import { setHtml as _setHtml, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("<div></div>", true)
}"
`;
+exports[`compiler v-bind > v-bind w/ svg elements 1`] = `
+"import { setDynamicProps as _setDynamicProps, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<svg></svg>", true, 1)
+
+export function render(_ctx) {
+ const n0 = t0()
+ _renderEffect(() => _setDynamicProps(n0, [_ctx.obj], true, true))
+ return n0
+}"
+`;
+
exports[`compiler v-bind > with constant value 1`] = `
"import { setProp as _setProp, template as _template } from 'vue';
const t0 = _template("<div e=\\"2\\" f=\\"foo1\\" g=\\"1\\" h=\\"1\\"></div>", true)
const template = '<div id="foo" class="bar"></div>'
expect(code).toMatchSnapshot()
expect(code).contains(JSON.stringify(template))
- expect(ir.template).toMatchObject([template])
+ expect([...ir.template.keys()]).toMatchObject([template])
expect(ir.block.effect).lengthOf(0)
})
const template = '<div id="foo"><span></span></div>'
expect(code).toMatchSnapshot()
expect(code).contains(JSON.stringify(template))
- expect(ir.template).toMatchObject([template])
+ expect([...ir.template.keys()]).toMatchObject([template])
expect(ir.block.effect).lengthOf(0)
})
<form><form/></form>`,
)
expect(code).toMatchSnapshot()
- expect(ir.template).toEqual(['<div>123</div>', '<p></p>', '<form></form>'])
+ expect([...ir.template.keys()]).toEqual([
+ '<div>123</div>',
+ '<p></p>',
+ '<form></form>',
+ ])
expect(ir.block.dynamic).toMatchObject({
children: [
{ id: 1, template: 1, children: [{ id: 0, template: 0 }] },
expect(code).toMatchSnapshot()
expect(code).contain('return null')
})
+
+ test('svg', () => {
+ const t = `<svg><circle r="40"></circle></svg>`
+ const { code, ir } = compileWithElementTransform(t)
+ expect(code).toMatchSnapshot()
+ expect(code).contains(
+ '_template("<svg><circle r=\\"40\\"></circle></svg>", true, 1)',
+ )
+ expect([...ir.template.keys()]).toMatchObject([t])
+ expect(ir.template.get(t)).toBe(1)
+ })
+
+ test('MathML', () => {
+ const t = `<math><mrow><mi>x</mi></mrow></math>`
+ const { code, ir } = compileWithElementTransform(t)
+ expect(code).toMatchSnapshot()
+ expect(code).contains(
+ '_template("<math><mrow><mi>x</mi></mrow></math>", true, 2)',
+ )
+ expect([...ir.template.keys()]).toMatchObject([t])
+ expect(ir.template.get(t)).toBe(2)
+ })
})
test('default slot outlet with fallback', () => {
const { ir, code } = compileWithSlotsOutlet(`<slot><div/></slot>`)
expect(code).toMatchSnapshot()
- expect(ir.template[0]).toBe('<div></div>')
+ expect([...ir.template.keys()][0]).toBe('<div></div>')
expect(ir.block.dynamic.children[0].operation).toMatchObject({
type: IRNodeTypes.SLOT_OUTLET_NODE,
id: 0,
`<slot name="foo"><div/></slot>`,
)
expect(code).toMatchSnapshot()
- expect(ir.template[0]).toBe('<div></div>')
+ expect([...ir.template.keys()][0]).toBe('<div></div>')
expect(ir.block.dynamic.children[0].operation).toMatchObject({
type: IRNodeTypes.SLOT_OUTLET_NODE,
id: 0,
`<slot :foo="bar"><div/></slot>`,
)
expect(code).toMatchSnapshot()
- expect(ir.template[0]).toBe('<div></div>')
+ expect([...ir.template.keys()][0]).toBe('<div></div>')
expect(ir.block.dynamic.children[0].operation).toMatchObject({
type: IRNodeTypes.SLOT_OUTLET_NODE,
id: 0,
`<slot name="foo" :foo="bar"><div/></slot>`,
)
expect(code).toMatchSnapshot()
- expect(ir.template[0]).toBe('<div></div>')
+ expect([...ir.template.keys()][0]).toBe('<div></div>')
expect(ir.block.dynamic.children[0].operation).toMatchObject({
type: IRNodeTypes.SLOT_OUTLET_NODE,
id: 0,
id: 0,
flags: DynamicFlag.REFERENCED,
})
- expect(ir.template).toEqual(['<div></div>'])
+ expect([...ir.template.keys()]).toEqual(['<div></div>'])
expect(ir.block.operation).lengthOf(1)
expect(ir.block.operation[0]).toMatchObject({
type: IRNodeTypes.SET_TEMPLATE_REF,
id: 0,
flags: DynamicFlag.REFERENCED,
})
- expect(ir.template).toEqual(['<div></div>'])
+ expect([...ir.template.keys()]).toEqual(['<div></div>'])
expect(ir.block.operation).toMatchObject([
{
type: IRNodeTypes.DECLARE_OLD_REF,
id: 0,
flags: DynamicFlag.REFERENCED,
})
- expect(ir.template).toEqual(['<div></div>'])
+ expect([...ir.template.keys()]).toEqual(['<div></div>'])
expect(ir.block.operation).toMatchObject([
{
type: IRNodeTypes.DECLARE_OLD_REF,
it('escapes raw static text when generating the template string', () => {
const { ir } = compileWithTextTransform('<code><script></code>')
- expect(ir.template).toContain('<code><script></code>')
- expect(ir.template).not.toContain('<code><script></code>')
+ expect([...ir.template.keys()]).toContain('<code><script></code>')
+ expect([...ir.template.keys()]).not.toContain('<code><script></code>')
})
test('constant text', () => {
id: 0,
flags: DynamicFlag.REFERENCED,
})
- expect(ir.template).toEqual(['<div></div>'])
+ expect([...ir.template.keys()]).toEqual(['<div></div>'])
expect(ir.block.effect).lengthOf(1)
expect(ir.block.effect[0].expressions).lengthOf(1)
expect(ir.block.effect[0].operations).lengthOf(1)
end: { line: 1, column: 19 },
},
})
- expect(ir.template).toEqual(['<div arg></div>'])
+ expect([...ir.template.keys()]).toEqual(['<div arg></div>'])
expect(code).matchSnapshot()
expect(code).contains(JSON.stringify('<div arg></div>'))
expect(code).contains('_setProp(n0, "value", _ctx.foo)')
})
+ test(':class w/ svg elements', () => {
+ const { code } = compileWithVBind(`
+ <svg :class="cls"/>
+ `)
+ expect(code).matchSnapshot()
+ expect(code).contains('_setAttr(n0, "class", _ctx.cls, true))')
+ })
+
+ test('v-bind w/ svg elements', () => {
+ const { code } = compileWithVBind(`
+ <svg v-bind="obj"/>
+ `)
+ expect(code).matchSnapshot()
+ expect(code).contains('_setDynamicProps(n0, [_ctx.obj], true, true))')
+ })
+
test('number value', () => {
const { code } = compileWithVBind(`<Comp :depth="0" />`)
expect(code).matchSnapshot()
expect(code).matchSnapshot()
expect(helpers).contains('createFor')
- expect(ir.template).toEqual(['<div> </div>'])
+ expect([...ir.template.keys()]).toEqual(['<div> </div>'])
expect(ir.block.dynamic.children[0].operation).toMatchObject({
type: IRNodeTypes.FOR,
id: 0,
`_createFor(() => (_for_item0.value), (_for_item1) => {`,
)
expect(code).contains(`_for_item1.value+_for_item0.value`)
- expect(ir.template).toEqual(['<span> </span>', '<div></div>'])
+ expect([...ir.template.keys()]).toEqual(['<span> </span>', '<div></div>'])
const parentOp = ir.block.dynamic.children[0].operation
expect(parentOp).toMatchObject({
type: IRNodeTypes.FOR,
expect(helpers).contains('setHtml')
// children should have been removed
- expect(ir.template).toEqual(['<div></div>'])
+ expect([...ir.template.keys()]).toEqual(['<div></div>'])
expect(ir.block.operation).toMatchObject([])
expect(ir.block.effect).toMatchObject([
expect(helpers).contains('createIf')
- expect(ir.template).toEqual(['<div> </div>'])
+ expect([...ir.template.keys()]).toEqual(['<div> </div>'])
const op = ir.block.dynamic.children[0].operation
expect(op).toMatchObject({
)
expect(code).matchSnapshot()
- expect(ir.template).toEqual(['<div></div>', 'hello', '<p> </p>'])
+ expect([...ir.template.keys()]).toEqual([
+ '<div></div>',
+ 'hello',
+ '<p> </p>',
+ ])
expect(ir.block.effect).toEqual([])
const op = ir.block.dynamic.children[0].operation as IfIRNode
expect(op.positive.effect).toMatchObject([
`<div v-if="ok">hello</div><div v-if="ok">hello</div>`,
)
expect(code).matchSnapshot()
- expect(ir.template).toEqual(['<div>hello</div>'])
+ expect([...ir.template.keys()]).toEqual(['<div>hello</div>'])
expect(ir.block.returns).toEqual([0, 3])
})
test('v-if + v-else', () => {
const { code, ir, helpers } = compileWithVIf(`<div v-if="ok"/><p v-else/>`)
expect(code).matchSnapshot()
- expect(ir.template).toEqual(['<div></div>', '<p></p>'])
+ expect([...ir.template.keys()]).toEqual(['<div></div>', '<p></p>'])
expect(helpers).contains('createIf')
expect(ir.block.effect).lengthOf(0)
`<div v-if="ok"/><p v-else-if="orNot"/>`,
)
expect(code).matchSnapshot()
- expect(ir.template).toEqual(['<div></div>', '<p></p>'])
+ expect([...ir.template.keys()]).toEqual(['<div></div>', '<p></p>'])
expect(ir.block.dynamic.children[0].operation).toMatchObject({
type: IRNodeTypes.IF,
`<div v-if="ok"/><p v-else-if="orNot"/><p v-else-if="false"/><template v-else>fine</template>`,
)
expect(code).matchSnapshot()
- expect(ir.template).toEqual(['<div></div>', '<p></p>', 'fine'])
+ expect([...ir.template.keys()]).toEqual(['<div></div>', '<p></p>', 'fine'])
expect(ir.block.returns).toEqual([0])
expect(ir.block.dynamic.children[0].operation).toMatchObject({
<div v-text="text" />
`)
expect(code).matchSnapshot()
- expect(ir.template).toEqual([
+ expect([...ir.template.keys()]).toEqual([
'<div></div>',
'<!--foo-->',
'<p></p>',
const { ir, code } = compileWithSlots(`<Comp><div/></Comp>`)
expect(code).toMatchSnapshot()
- expect(ir.template).toEqual(['<div></div>'])
+ expect([...ir.template.keys()]).toEqual(['<div></div>'])
expect(ir.block.dynamic.children[0].operation).toMatchObject({
type: IRNodeTypes.CREATE_COMPONENT_NODE,
id: 1,
)
expect(code).toMatchSnapshot()
- expect(ir.template).toEqual(['foo', 'bar', '<span></span>'])
+ expect([...ir.template.keys()]).toEqual(['foo', 'bar', '<span></span>'])
expect(ir.block.dynamic.children[0].operation).toMatchObject({
type: IRNodeTypes.CREATE_COMPONENT_NODE,
id: 4,
])
// children should have been removed
- expect(ir.template).toEqual(['<div> </div>'])
+ expect([...ir.template.keys()]).toEqual(['<div> </div>'])
expect(ir.block.effect).toMatchObject([
{
import {
canSetValueDirectly,
capitalize,
+ extend,
isSVGTag,
shouldSetAsAttr,
toHandlerKey,
export type HelperConfig = {
name: VaporHelper
needKey?: boolean
+ isSVG?: boolean
acceptRoot?: boolean
}
setAttr: { name: 'setAttr', needKey: true },
setProp: { name: 'setProp', needKey: true },
setDOMProp: { name: 'setDOMProp', needKey: true },
- setDynamicProps: { name: 'setDynamicProps' },
} as const satisfies Partial<Record<VaporHelper, HelperConfig>>
// only the static key prop will reach here
`n${oper.element}`,
resolvedHelper.needKey ? genExpression(key, context) : false,
propValue,
+ resolvedHelper.isSVG && 'true',
),
]
}
context: CodegenContext,
): CodeFragment[] {
const { helper } = context
+ const isSVG = isSVGTag(oper.tag)
const values = oper.props.map(props =>
Array.isArray(props)
? genLiteralObjectProps(props, context) // static and dynamic arg props
`n${oper.element}`,
genMulti(DELIMITERS_ARRAY, ...values),
oper.root && 'true',
+ isSVG && 'true',
),
]
}
modifier: '.' | '^' | undefined,
): HelperConfig {
const tagName = tag.toUpperCase()
+ const isSVG = isSVGTag(tag)
+
+ // 1. SVG: always attribute
+ if (isSVG) {
+ return extend({ isSVG: true }, helpers.setAttr)
+ }
+
if (modifier) {
if (modifier === '.') {
return getSpecialHelper(key, tagName) || helpers.setDOMProp
}
}
- // 1. special handling for value / style / class / textContent / innerHTML
+ // 2. special handling for value / style / class / textContent / innerHTML
const helper = getSpecialHelper(key, tagName)
if (helper) {
return helper
}
- // 2. Aria DOM properties shared between all Elements in
+ // 3. Aria DOM properties shared between all Elements in
// https://developer.mozilla.org/en-US/docs/Web/API/Element
if (/aria[A-Z]/.test(key)) {
return helpers.setDOMProp
}
- // 3. SVG: always attribute
- if (isSVGTag(tag)) {
- // TODO pass svg flag
- return helpers.setAttr
- }
-
// 4. respect shouldSetAsAttr used in vdom and setDynamicProp for consistency
// also fast path for presence of hyphen (covers data-* and aria-*)
if (shouldSetAsAttr(tagName, key) || key.includes('-')) {
import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils'
export function genTemplates(
- templates: string[],
+ templates: Map<string, number>,
rootIndex: number | undefined,
context: CodegenContext,
): string {
- return templates
- .map(
- (template, i) =>
- `const ${context.tName(i)} = ${context.helper('template')}(${JSON.stringify(
- template,
- )}${i === rootIndex ? ', true' : ''})\n`,
+ const result: string[] = []
+ let i = 0
+ templates.forEach((ns, template) => {
+ result.push(
+ `const ${context.tName(i)} = ${context.helper('template')}(${JSON.stringify(
+ template,
+ )}${i === rootIndex ? ', true' : ns ? ', false' : ''}${ns ? `, ${ns}` : ''})\n`,
)
- .join('')
+ i++
+ })
+ return result.join('')
}
export function genSelf(
SimpleExpressionNode,
TemplateChildNode,
} from '@vue/compiler-dom'
-import type { Prettify } from '@vue/shared'
+import type { Namespace, Prettify } from '@vue/shared'
import type { DirectiveTransform, NodeTransform } from '../transform'
import type { IRProp, IRProps, IRSlots } from './component'
type: IRNodeTypes.ROOT
node: RootNode
source: string
- template: string[]
+ template: Map<string, Namespace>
+ templateIndexMap: Map<string, number>
rootTemplateIndex?: number
component: Set<string>
directive: Set<string>
element: number
props: IRProps[]
root: boolean
+ tag: string
}
export interface SetDynamicEventsIRNode extends BaseIRNode {
type ElementNode,
ElementTypes,
NodeTypes,
+ type PlainElementNode,
type RootNode,
type SimpleExpressionNode,
type TemplateChildNode,
}
pushTemplate(content: string): number {
- const existing = this.ir.template.findIndex(
- template => template === content,
- )
- if (existing !== -1) return existing
- this.ir.template.push(content)
- return this.ir.template.length - 1
+ const existingIndex = this.ir.templateIndexMap.get(content)
+ if (existingIndex !== undefined) {
+ return existingIndex
+ }
+
+ const newIndex = this.ir.template.size
+ this.ir.template.set(content, (this.node as PlainElementNode).ns)
+ this.ir.templateIndexMap.set(content, newIndex)
+ return newIndex
}
registerTemplate(): number {
if (!this.template) return -1
type: IRNodeTypes.ROOT,
node,
source: node.source,
- template: [],
+ template: new Map<string, number>(),
+ templateIndexMap: new Map<string, number>(),
component: new Set(),
directive: new Set(),
block: newBlock(node),
element: context.reference(),
props: dynamicArgs,
root: singleRoot,
+ tag,
},
getEffectIndex,
)
}
if (singleRoot) {
- context.ir.rootTemplateIndex = context.ir.template.length
+ context.ir.rootTemplateIndex = context.ir.template.size
}
if (
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')
patchProp(el, 'textContent', null, 'foo', 'svg')
expect(el.attributes.length).toBe(0)
const positionMap = new WeakMap<VNode, Position>()
const newPositionMap = new WeakMap<VNode, Position>()
-export const moveCbKey: unique symbol = Symbol('_moveCb')
+const moveCbKey: unique symbol = Symbol('_moveCb')
const enterCbKey = Symbol('_enterCb')
export type TransitionGroupProps = Omit<TransitionProps, 'mode'> & {
vModelSelectInit,
vModelSetSelected,
} from './directives/vModel'
+/**
+ * @internal
+ */
+export { svgNS } from './nodeOps'
+/**
+ * @internal
+ */
+export { xlinkNS } from './modules/attrs'
/**
* @internal
*/
resolveTransitionProps,
TransitionPropsValidators,
forceReflow,
- addTransitionClass,
- removeTransitionClass,
type ElementWithTransition,
} from './components/Transition'
/**
export {
hasCSSTransform,
callPendingCbs,
- moveCbKey,
handleMovedChildren,
baseApplyTranslation,
} from './components/TransitionGroup'
--- /dev/null
+import { makeRender } from '../_utils'
+import { template } from '../../src/dom/template'
+import { child } from '../../src/dom/node'
+import { setClass } from '../../src/dom/prop'
+import { renderEffect } from '../../src'
+import { nextTick, ref } from '@vue/runtime-dom'
+
+const define = makeRender()
+
+describe('MathML support', () => {
+ afterEach(() => {
+ document.body.innerHTML = ''
+ })
+
+ test('should mount elements with correct html namespace', () => {
+ define({
+ setup() {
+ const t0 = 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"></div>
+ <svg id="e5"></svg>
+ </annotation-xml>
+ </semantics>
+ </math>`,
+ true,
+ 2,
+ )
+ const n0 = t0()
+ return n0
+ },
+ }).render()
+
+ 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 cls = ref('foo')
+ define({
+ setup() {
+ const t0 = template(
+ '<div><math id="f1"><annotation encoding="text/html"><a id="f2"></a></annotation></math></div>',
+ true,
+ )
+
+ const n2 = t0() as HTMLElement
+ const n1 = child(n2) as HTMLElement
+ const p0 = child(n1) as HTMLElement
+ const n0 = child(p0) as HTMLElement
+ renderEffect(() => {
+ const _cls = cls.value
+ setClass(n1, _cls)
+ setClass(n0, _cls)
+ })
+ return n2
+ },
+ }).render()
+
+ const f1 = document.querySelector('#f1')!
+ const f2 = document.querySelector('#f2')!
+ expect(f1.getAttribute('class')).toBe('foo')
+ expect(f2.className).toBe('foo')
+
+ cls.value = 'bar'
+ await nextTick()
+ expect(f1.getAttribute('class')).toBe('bar')
+ expect(f2.className).toBe('bar')
+ })
+})
} from '../../src/dom/prop'
import { setStyle } from '../../src/dom/prop'
import { VaporComponentInstance, createComponent } from '../../src/component'
-import { ref, setCurrentInstance } from '@vue/runtime-dom'
+import { ref, setCurrentInstance, svgNS, xlinkNS } from '@vue/runtime-dom'
import { makeRender } from '../_utils'
import {
createDynamicComponent,
key: string,
value: any,
el = element.cloneNode(true) as HTMLElement,
+ isSVG: boolean = false,
) {
- _setDynamicProp(el, key, value)
+ _setDynamicProp(el, key, value, isSVG)
return el
}
expect(res.textContent).toBe('foo')
})
- test.todo('should be able to set something on SVG')
+ test('set class w/ SVG', () => {
+ const el = document.createElementNS(svgNS, 'svg') as any
+ setDynamicProp('class', 'foo', el, true)
+ expect(el.getAttribute('class')).toBe('foo')
+ })
+
+ test('set class incremental w/ SVG', () => {
+ const el = document.createElementNS(svgNS, 'svg') as any
+ el.setAttribute('class', 'bar')
+ el.$root = true
+ setDynamicProp('class', 'foo', el, true)
+ expect(el.getAttribute('class')).toBe('bar foo')
+ })
+
+ test('set xlink attributes w/ SVG', () => {
+ const el = document.createElementNS(
+ 'http://www.w3.org/2000/svg',
+ 'use',
+ ) as any
+ setDynamicProp('xlink:href', 'a', el, true)
+ expect(el.getAttributeNS(xlinkNS, 'href')).toBe('a')
+ setDynamicProp('xlink:href', null, el, true)
+ expect(el.getAttributeNS(xlinkNS, 'href')).toBe(null)
+ })
+
+ test('set textContent attributes w/ SVG', () => {
+ const el = document.createElementNS(
+ 'http://www.w3.org/2000/svg',
+ 'use',
+ ) as any
+ setDynamicProp('textContent', 'foo', el, true)
+ expect(el.attributes.length).toBe(0)
+ expect(el.innerHTML).toBe('foo')
+ })
})
describe('setDynamicProps', () => {
--- /dev/null
+import { makeRender } from '../_utils'
+import { template } from '../../src/dom/template'
+import { child } from '../../src/dom/node'
+import { setAttr, setClass } from '../../src/dom/prop'
+import { renderEffect } from '../../src'
+import { nextTick, ref } from '@vue/runtime-dom'
+
+const define = makeRender()
+
+describe('SVG support', () => {
+ afterEach(() => {
+ document.body.innerHTML = ''
+ })
+
+ test('should mount elements with correct html namespace', () => {
+ define({
+ setup() {
+ const t0 = template(
+ `<div id="e0">
+ <svg id="e1">
+ <foreignObject id="e2">
+ <div id="e3"></div>
+ <svg id="e4"></svg>
+ <math id="e5"></math>
+ </foreignObject>
+ </svg>
+ </div>`,
+ true,
+ )
+ return t0()
+ },
+ }).render()
+
+ const e0 = document.getElementById('e0')!
+ expect(e0.namespaceURI).toMatch('xhtml')
+ 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 () => {
+ const cls = ref('foo')
+ define({
+ setup() {
+ const t0 = template(
+ '<div><svg id="f1"><foreignObject><div id="f2">hi</div></foreignObject></svg></div>',
+ true,
+ )
+ const n2 = t0() as HTMLElement
+ const n1 = child(n2) as HTMLElement
+ const p0 = child(n1) as HTMLElement
+ const n0 = child(p0) as HTMLElement
+ renderEffect(() => {
+ const _cls = cls.value
+ setAttr(n1, 'class', _cls)
+ setClass(n0, _cls)
+ })
+ return n2
+ },
+ }).render()
+ const f1 = document.querySelector('#f1')!
+ const f2 = document.querySelector('#f2')!
+ expect(f1.getAttribute('class')).toBe('foo')
+ expect(f2.className).toBe('foo')
+
+ cls.value = 'bar'
+ await nextTick()
+ expect(f1.getAttribute('class')).toBe('bar')
+ expect(f2.className).toBe('bar')
+ })
+})
vShowHidden,
warn,
warnPropMismatch,
+ xlinkNS,
} from '@vue/runtime-dom'
import {
type VaporComponentInstance,
}
}
-export function setAttr(el: any, key: string, value: any): void {
+export function setAttr(
+ el: any,
+ key: string,
+ value: any,
+ isSVG: boolean = false,
+): void {
if (!isApplyingFallthroughProps && el.$root && hasFallthroughKey(key)) {
return
}
if (value !== el[`$${key}`]) {
el[`$${key}`] = value
- if (value != null) {
- el.setAttribute(key, value)
+ if (isSVG && key.startsWith('xlink:')) {
+ if (value != null) {
+ el.setAttributeNS(xlinkNS, key, value)
+ } else {
+ el.removeAttributeNS(xlinkNS, key.slice(6, key.length))
+ }
} else {
- el.removeAttribute(key)
+ if (value != null) {
+ el.setAttribute(key, value)
+ } else {
+ el.removeAttribute(key)
+ }
}
}
}
needRemove && el.removeAttribute(key)
}
-export function setClass(el: TargetElement, value: any): void {
+export function setClass(
+ el: TargetElement,
+ value: any,
+ isSVG: boolean = false,
+): void {
if (el.$root) {
setClassIncremental(el, value)
} else {
}
if (value !== el.$cls) {
- el.className = el.$cls = value
+ if (isSVG) {
+ el.setAttribute('class', (el.$cls = value))
+ } else {
+ el.className = el.$cls = value
+ }
}
}
}
}
}
-export function setDynamicProps(el: any, args: any[]): void {
+export function setDynamicProps(
+ el: any,
+ args: any[],
+ root?: boolean,
+ isSVG?: boolean,
+): void {
const props = args.length > 1 ? mergeProps(...args) : args[0]
const cacheKey = `$dprops${isApplyingFallthroughProps ? '$' : ''}`
const prevKeys = el[cacheKey] as string[]
+ if (root) el.$root = root
if (prevKeys) {
for (const key of prevKeys) {
if (!(key in props)) {
- setDynamicProp(el, key, null)
+ setDynamicProp(el, key, null, isSVG)
}
}
}
for (const key of (el[cacheKey] = Object.keys(props))) {
- setDynamicProp(el, key, props[key])
+ setDynamicProp(el, key, props[key], isSVG)
}
}
el: TargetElement,
key: string,
value: any,
+ isSVG: boolean = false,
): void {
- // TODO
- const isSVG = false
let forceHydrate = false
if (key === 'class') {
- setClass(el, value)
+ setClass(el, value, isSVG)
} else if (key === 'style') {
setStyle(el, value)
} else if (isOn(key)) {
setDOMProp(el, key, value, forceHydrate)
}
} else {
- setAttr(el, key, value)
+ setAttr(el, key, value, isSVG)
}
return value
}
import { adoptTemplate, currentHydrationNode, isHydrating } from './hydration'
-import { _child, createElement, createTextNode } from './node'
+import { type Namespace, Namespaces } from '@vue/shared'
+import { _child, createTextNode } from './node'
let t: HTMLTemplateElement
/*! #__NO_SIDE_EFFECTS__ */
-export function template(html: string, root?: boolean) {
+export function template(html: string, root?: boolean, ns?: Namespace) {
let node: Node
return (): Node & { $root?: true } => {
if (isHydrating) {
return createTextNode(html)
}
if (!node) {
- t = t || createElement('template')
- t.innerHTML = html
- node = _child(t.content)
+ t = t || document.createElement('template')
+ if (ns) {
+ const tag = ns === Namespaces.SVG ? 'svg' : 'math'
+ t.innerHTML = `<${tag}>${html}</${tag}>`
+ node = _child(_child(t.content) as ParentNode)
+ } else {
+ t.innerHTML = html
+ node = _child(t.content)
+ }
}
const ret = node.cloneNode(true)
if (root) (ret as any).$root = true
--- /dev/null
+// Vue template is a platform-agnostic superset of HTML (syntax only).
+// More namespaces can be declared by platform specific compilers.
+
+export type Namespace = number
+
+export enum Namespaces {
+ HTML,
+ SVG,
+ MATH_ML,
+}
export * from './normalizeProp'
export * from './domTagConfig'
export * from './domAttrConfig'
+export * from './domNamespace'
export * from './escapeHtml'
export * from './looseEqual'
export * from './toDisplayString'