diff --git a/packages/compiler-core/__tests__/parse.spec.ts b/packages/compiler-core/__tests__/parse.spec.ts index cdc2b09fd48..306d8089ff2 100644 --- a/packages/compiler-core/__tests__/parse.spec.ts +++ b/packages/compiler-core/__tests__/parse.spec.ts @@ -7,7 +7,6 @@ import { type ElementNode, ElementTypes, type InterpolationNode, - Namespaces, NodeTypes, type Position, type TextNode, @@ -15,6 +14,7 @@ import { import { baseParse } from '../src/parser' import type { Program } from '@babel/types' +import { Namespaces } from '@vue/shared' describe('compiler: parse', () => { describe('Text', () => { diff --git a/packages/compiler-core/__tests__/testUtils.ts b/packages/compiler-core/__tests__/testUtils.ts index a2525e0cab9..6ef41b48b07 100644 --- a/packages/compiler-core/__tests__/testUtils.ts +++ b/packages/compiler-core/__tests__/testUtils.ts @@ -1,7 +1,6 @@ import { type ElementNode, ElementTypes, - Namespaces, NodeTypes, type Property, type SimpleExpressionNode, @@ -9,6 +8,7 @@ import { locStub, } from '../src' import { + Namespaces, PatchFlagNames, type PatchFlags, type ShapeFlags, diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index bae13372a98..929a86e4bf3 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -1,4 +1,4 @@ -import { type PatchFlags, isString } from '@vue/shared' +import { type Namespace, type PatchFlags, isString } from '@vue/shared' import { CREATE_BLOCK, CREATE_ELEMENT_BLOCK, @@ -16,16 +16,6 @@ import type { PropsExpression } from './transforms/transformElement' 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, diff --git a/packages/compiler-core/src/options.ts b/packages/compiler-core/src/options.ts index 9983071609e..4c2b2f77070 100644 --- a/packages/compiler-core/src/options.ts +++ b/packages/compiler-core/src/options.ts @@ -1,10 +1,5 @@ -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, diff --git a/packages/compiler-core/src/parser.ts b/packages/compiler-core/src/parser.ts index 3eb3a976f4e..2bea73986d6 100644 --- a/packages/compiler-core/src/parser.ts +++ b/packages/compiler-core/src/parser.ts @@ -5,7 +5,6 @@ import { type ElementNode, ElementTypes, type ForParseResult, - Namespaces, NodeTypes, type RootNode, type SimpleExpressionNode, @@ -14,6 +13,7 @@ import { createRoot, createSimpleExpression, } from './ast' +import { Namespaces } from '@vue/shared' import type { ParserOptions } from './options' import Tokenizer, { CharCodes, diff --git a/packages/compiler-dom/__tests__/parse.spec.ts b/packages/compiler-dom/__tests__/parse.spec.ts index 7418b8e33fb..e02b8c7b87a 100644 --- a/packages/compiler-dom/__tests__/parse.spec.ts +++ b/packages/compiler-dom/__tests__/parse.spec.ts @@ -4,12 +4,12 @@ import { 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', () => { @@ -491,6 +491,17 @@ describe('DOM parser', () => { expect(element.ns).toBe(Namespaces.SVG) }) + test('SVG tags without explicit root', () => { + const ast = parse('', 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('', parserOptions) const elementHtml = ast.children[0] as ElementNode @@ -500,6 +511,17 @@ describe('DOM parser', () => { expect(element.ns).toBe(Namespaces.MATH_ML) }) + test('MATH tags without explicit root', () => { + const ast = parse('', 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('', { ...parserOptions, diff --git a/packages/compiler-dom/src/parserOptions.ts b/packages/compiler-dom/src/parserOptions.ts index 7da13bf534d..4106bd5af31 100644 --- a/packages/compiler-dom/src/parserOptions.ts +++ b/packages/compiler-dom/src/parserOptions.ts @@ -1,5 +1,11 @@ -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' @@ -24,7 +30,7 @@ export const parserOptions: ParserOptions = { 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 ( @@ -57,10 +63,10 @@ export const parserOptions: ParserOptions = { } if (ns === Namespaces.HTML) { - if (tag === 'svg') { + if (isSVGTag(tag)) { return Namespaces.SVG } - if (tag === 'math') { + if (isMathMLTag(tag)) { return Namespaces.MATH_ML } } diff --git a/packages/compiler-dom/src/transforms/stringifyStatic.ts b/packages/compiler-dom/src/transforms/stringifyStatic.ts index cd8f1a9d184..d35211f5d6b 100644 --- a/packages/compiler-dom/src/transforms/stringifyStatic.ts +++ b/packages/compiler-dom/src/transforms/stringifyStatic.ts @@ -9,7 +9,6 @@ import { ElementTypes, type ExpressionNode, type HoistTransform, - Namespaces, NodeTypes, type PlainElementNode, type SimpleExpressionNode, @@ -20,6 +19,7 @@ import { isStaticArgOf, } from '@vue/compiler-core' import { + Namespaces, escapeHtml, isArray, isBooleanAttr, diff --git a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts index cad1ee81028..5cf9c736a84 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts @@ -10,7 +10,6 @@ import { type ExpressionNode, type FunctionExpression, type JSChildNode, - Namespaces, type NodeTransform, NodeTypes, RESOLVE_DYNAMIC_COMPONENT, @@ -55,7 +54,14 @@ import { 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, diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap index 7aa56aa9c2f..5a3c13946bb 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap @@ -1,5 +1,15 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`compiler: element transform > MathML 1`] = ` +"import { template as _template } from 'vue'; +const t0 = _template("x", true, 2) + +export function render(_ctx) { + const n0 = t0() + return n0 +}" +`; + exports[`compiler: element transform > component > cache v-on expression with unique handler name 1`] = ` "import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; @@ -407,6 +417,16 @@ export function render(_ctx) { }" `; +exports[`compiler: element transform > svg 1`] = ` +"import { template as _template } from 'vue'; +const t0 = _template("", 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("
", true) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap index 4ea0db55fe5..edac6aef96a 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap @@ -465,6 +465,17 @@ export function render(_ctx) { }" `; +exports[`compiler v-bind > :class w/ svg elements 1`] = ` +"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("", true, 1) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => _setAttr(n0, "class", _ctx.cls)) + return n0 +}" +`; + exports[`compiler v-bind > :innerHTML 1`] = ` "import { setHtml as _setHtml, renderEffect as _renderEffect, template as _template } from 'vue'; const t0 = _template("
", true) diff --git a/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts index a693db4ad39..43280f6061a 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts @@ -579,7 +579,7 @@ describe('compiler: element transform', () => { const template = '
' 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) }) @@ -591,7 +591,7 @@ describe('compiler: element transform', () => { const template = '
' 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) }) @@ -937,7 +937,11 @@ describe('compiler: element transform', () => {
`, ) expect(code).toMatchSnapshot() - expect(ir.template).toEqual(['
123
', '

', '
']) + expect([...ir.template.keys()]).toEqual([ + '
123
', + '

', + '
', + ]) expect(ir.block.dynamic).toMatchObject({ children: [ { id: 1, template: 1, children: [{ id: 0, template: 0 }] }, @@ -956,4 +960,26 @@ describe('compiler: element transform', () => { expect(code).toMatchSnapshot() expect(code).contain('return null') }) + + test('svg', () => { + const t = `` + const { code, ir } = compileWithElementTransform(t) + expect(code).toMatchSnapshot() + expect(code).contains( + '_template("", true, 1)', + ) + expect([...ir.template.keys()]).toMatchObject([t]) + expect(ir.template.get(t)).toBe(1) + }) + + test('MathML', () => { + const t = `x` + const { code, ir } = compileWithElementTransform(t) + expect(code).toMatchSnapshot() + expect(code).contains( + '_template("x", true, 2)', + ) + expect([...ir.template.keys()]).toMatchObject([t]) + expect(ir.template.get(t)).toBe(2) + }) }) diff --git a/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts index 389c665a12f..99daa571f60 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts @@ -155,7 +155,7 @@ describe('compiler: transform outlets', () => { test('default slot outlet with fallback', () => { const { ir, code } = compileWithSlotsOutlet(`
`) expect(code).toMatchSnapshot() - expect(ir.template[0]).toBe('
') + expect([...ir.template.keys()][0]).toBe('
') expect(ir.block.dynamic.children[0].operation).toMatchObject({ type: IRNodeTypes.SLOT_OUTLET_NODE, id: 0, @@ -175,7 +175,7 @@ describe('compiler: transform outlets', () => { `
`, ) expect(code).toMatchSnapshot() - expect(ir.template[0]).toBe('
') + expect([...ir.template.keys()][0]).toBe('
') expect(ir.block.dynamic.children[0].operation).toMatchObject({ type: IRNodeTypes.SLOT_OUTLET_NODE, id: 0, @@ -195,7 +195,7 @@ describe('compiler: transform outlets', () => { `
`, ) expect(code).toMatchSnapshot() - expect(ir.template[0]).toBe('
') + expect([...ir.template.keys()][0]).toBe('
') expect(ir.block.dynamic.children[0].operation).toMatchObject({ type: IRNodeTypes.SLOT_OUTLET_NODE, id: 0, @@ -216,7 +216,7 @@ describe('compiler: transform outlets', () => { `
`, ) expect(code).toMatchSnapshot() - expect(ir.template[0]).toBe('
') + expect([...ir.template.keys()][0]).toBe('
') expect(ir.block.dynamic.children[0].operation).toMatchObject({ type: IRNodeTypes.SLOT_OUTLET_NODE, id: 0, diff --git a/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts index 2c883d10cc6..82d07ce0ab6 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts @@ -30,7 +30,7 @@ describe('compiler: template ref transform', () => { id: 0, flags: DynamicFlag.REFERENCED, }) - expect(ir.template).toEqual(['
']) + expect([...ir.template.keys()]).toEqual(['
']) expect(ir.block.operation).lengthOf(1) expect(ir.block.operation[0]).toMatchObject({ type: IRNodeTypes.SET_TEMPLATE_REF, @@ -66,7 +66,7 @@ describe('compiler: template ref transform', () => { id: 0, flags: DynamicFlag.REFERENCED, }) - expect(ir.template).toEqual(['
']) + expect([...ir.template.keys()]).toEqual(['
']) expect(ir.block.operation).toMatchObject([ { type: IRNodeTypes.DECLARE_OLD_REF, @@ -104,7 +104,7 @@ describe('compiler: template ref transform', () => { id: 0, flags: DynamicFlag.REFERENCED, }) - expect(ir.template).toEqual(['
']) + expect([...ir.template.keys()]).toEqual(['
']) expect(ir.block.operation).toMatchObject([ { type: IRNodeTypes.DECLARE_OLD_REF, diff --git a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts index e96186c275c..60cd9d986ed 100644 --- a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts @@ -23,7 +23,7 @@ describe('compiler v-bind', () => { id: 0, flags: DynamicFlag.REFERENCED, }) - expect(ir.template).toEqual(['
']) + expect([...ir.template.keys()]).toEqual(['
']) expect(ir.block.effect).lengthOf(1) expect(ir.block.effect[0].expressions).lengthOf(1) expect(ir.block.effect[0].operations).lengthOf(1) @@ -241,7 +241,7 @@ describe('compiler v-bind', () => { end: { line: 1, column: 19 }, }, }) - expect(ir.template).toEqual(['
']) + expect([...ir.template.keys()]).toEqual(['
']) expect(code).matchSnapshot() expect(code).contains(JSON.stringify('
')) @@ -656,6 +656,14 @@ describe('compiler v-bind', () => { expect(code).contains('_setProp(n0, "value", _ctx.foo)') }) + test(':class w/ svg elements', () => { + const { code } = compileWithVBind(` + + `) + expect(code).matchSnapshot() + expect(code).contains('_setAttr(n0, "class", _ctx.cls))') + }) + test('number value', () => { const { code } = compileWithVBind(``) expect(code).matchSnapshot() diff --git a/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts b/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts index 7357ad84fef..011eb0ca92c 100644 --- a/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts @@ -32,7 +32,7 @@ describe('compiler: v-for', () => { expect(code).matchSnapshot() expect(helpers).contains('createFor') - expect(ir.template).toEqual(['
']) + expect([...ir.template.keys()]).toEqual(['
']) expect(ir.block.dynamic.children[0].operation).toMatchObject({ type: IRNodeTypes.FOR, id: 0, @@ -156,7 +156,7 @@ describe('compiler: v-for', () => { `_createFor(() => (_for_item0.value), (_for_item1) => {`, ) expect(code).contains(`_for_item1.value+_for_item0.value`) - expect(ir.template).toEqual([' ', '
']) + expect([...ir.template.keys()]).toEqual([' ', '
']) const parentOp = ir.block.dynamic.children[0].operation expect(parentOp).toMatchObject({ type: IRNodeTypes.FOR, diff --git a/packages/compiler-vapor/__tests__/transforms/vHtml.spec.ts b/packages/compiler-vapor/__tests__/transforms/vHtml.spec.ts index 0de0b6abca6..f297362b2cb 100644 --- a/packages/compiler-vapor/__tests__/transforms/vHtml.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vHtml.spec.ts @@ -66,7 +66,7 @@ describe('v-html', () => { expect(helpers).contains('setHtml') // children should have been removed - expect(ir.template).toEqual(['
']) + expect([...ir.template.keys()]).toEqual(['
']) expect(ir.block.operation).toMatchObject([]) expect(ir.block.effect).toMatchObject([ diff --git a/packages/compiler-vapor/__tests__/transforms/vIf.spec.ts b/packages/compiler-vapor/__tests__/transforms/vIf.spec.ts index e5fd61add2e..76a1bde6123 100644 --- a/packages/compiler-vapor/__tests__/transforms/vIf.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vIf.spec.ts @@ -32,7 +32,7 @@ describe('compiler: v-if', () => { expect(helpers).contains('createIf') - expect(ir.template).toEqual(['
']) + expect([...ir.template.keys()]).toEqual(['
']) const op = ir.block.dynamic.children[0].operation expect(op).toMatchObject({ @@ -68,7 +68,11 @@ describe('compiler: v-if', () => { ) expect(code).matchSnapshot() - expect(ir.template).toEqual(['
', 'hello', '

']) + expect([...ir.template.keys()]).toEqual([ + '
', + 'hello', + '

', + ]) expect(ir.block.effect).toEqual([]) const op = ir.block.dynamic.children[0].operation as IfIRNode expect(op.positive.effect).toMatchObject([ @@ -103,7 +107,7 @@ describe('compiler: v-if', () => { `
hello
hello
`, ) expect(code).matchSnapshot() - expect(ir.template).toEqual(['
hello
']) + expect([...ir.template.keys()]).toEqual(['
hello
']) expect(ir.block.returns).toEqual([0, 3]) }) @@ -113,7 +117,7 @@ describe('compiler: v-if', () => { test('v-if + v-else', () => { const { code, ir, helpers } = compileWithVIf(`

`) expect(code).matchSnapshot() - expect(ir.template).toEqual(['

', '

']) + expect([...ir.template.keys()]).toEqual(['
', '

']) expect(helpers).contains('createIf') expect(ir.block.effect).lengthOf(0) @@ -146,7 +150,7 @@ describe('compiler: v-if', () => { `

`, ) expect(code).matchSnapshot() - expect(ir.template).toEqual(['

', '

']) + expect([...ir.template.keys()]).toEqual(['
', '

']) expect(ir.block.dynamic.children[0].operation).toMatchObject({ type: IRNodeTypes.IF, @@ -185,7 +189,7 @@ describe('compiler: v-if', () => { `

`, ) expect(code).matchSnapshot() - expect(ir.template).toEqual(['

', '

', 'fine']) + expect([...ir.template.keys()]).toEqual(['
', '

', 'fine']) expect(ir.block.returns).toEqual([0]) expect(ir.block.dynamic.children[0].operation).toMatchObject({ @@ -236,7 +240,7 @@ describe('compiler: v-if', () => {
`) expect(code).matchSnapshot() - expect(ir.template).toEqual([ + expect([...ir.template.keys()]).toEqual([ '
', '', '

', diff --git a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts index 909162fe3ca..2a462479c3e 100644 --- a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts @@ -35,7 +35,7 @@ describe('compiler: transform slot', () => { const { ir, code } = compileWithSlots(`
`) expect(code).toMatchSnapshot() - expect(ir.template).toEqual(['
']) + expect([...ir.template.keys()]).toEqual(['
']) expect(ir.block.dynamic.children[0].operation).toMatchObject({ type: IRNodeTypes.CREATE_COMPONENT_NODE, id: 1, @@ -163,7 +163,7 @@ describe('compiler: transform slot', () => { ) expect(code).toMatchSnapshot() - expect(ir.template).toEqual(['foo', 'bar', '']) + expect([...ir.template.keys()]).toEqual(['foo', 'bar', '']) expect(ir.block.dynamic.children[0].operation).toMatchObject({ type: IRNodeTypes.CREATE_COMPONENT_NODE, id: 4, diff --git a/packages/compiler-vapor/__tests__/transforms/vText.spec.ts b/packages/compiler-vapor/__tests__/transforms/vText.spec.ts index 4f074fee87e..7ebac8554a8 100644 --- a/packages/compiler-vapor/__tests__/transforms/vText.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vText.spec.ts @@ -68,7 +68,7 @@ describe('v-text', () => { ]) // children should have been removed - expect(ir.template).toEqual(['
']) + expect([...ir.template.keys()]).toEqual(['
']) expect(ir.block.effect).toMatchObject([ { diff --git a/packages/compiler-vapor/src/generators/prop.ts b/packages/compiler-vapor/src/generators/prop.ts index 42f063331fc..392420613f5 100644 --- a/packages/compiler-vapor/src/generators/prop.ts +++ b/packages/compiler-vapor/src/generators/prop.ts @@ -169,6 +169,14 @@ function getRuntimeHelper( modifier: '.' | '^' | undefined, ): HelperConfig { const tagName = tag.toUpperCase() + const isSVG = isSVGTag(tag) + + // 1. SVG: always attribute + if (isSVG) { + // TODO pass svg flag + return helpers.setAttr + } + if (modifier) { if (modifier === '.') { return getSpecialHelper(key, tagName) || helpers.setDOMProp @@ -177,24 +185,18 @@ function getRuntimeHelper( } } - // 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('-')) { diff --git a/packages/compiler-vapor/src/generators/template.ts b/packages/compiler-vapor/src/generators/template.ts index 5a066b09e9a..e3cca1b48b0 100644 --- a/packages/compiler-vapor/src/generators/template.ts +++ b/packages/compiler-vapor/src/generators/template.ts @@ -5,18 +5,21 @@ import { genOperationWithInsertionState } from './operation' import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils' export function genTemplates( - templates: string[], + templates: Map, rootIndex: number | undefined, { helper }: CodegenContext, ): string { - return templates - .map( - (template, i) => - `const t${i} = ${helper('template')}(${JSON.stringify( - template, - )}${i === rootIndex ? ', true' : ''})\n`, + const result: string[] = [] + let i = 0 + templates.forEach((ns, template) => { + result.push( + `const t${i} = ${helper('template')}(${JSON.stringify( + template, + )}${i === rootIndex ? ', true' : ns ? ', false' : ''}${ns ? `, ${ns}` : ''})\n`, ) - .join('') + i++ + }) + return result.join('') } export function genSelf( diff --git a/packages/compiler-vapor/src/ir/index.ts b/packages/compiler-vapor/src/ir/index.ts index 18f0139ab56..fd4eefd559b 100644 --- a/packages/compiler-vapor/src/ir/index.ts +++ b/packages/compiler-vapor/src/ir/index.ts @@ -5,7 +5,7 @@ import type { 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' @@ -59,7 +59,8 @@ export interface RootIRNode { type: IRNodeTypes.ROOT node: RootNode source: string - template: string[] + template: Map + templateIndexMap: Map rootTemplateIndex?: number component: Set directive: Set diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index 946c89b734a..e993daf4b0c 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -6,6 +6,7 @@ import { type ElementNode, ElementTypes, NodeTypes, + type PlainElementNode, type RootNode, type SimpleExpressionNode, type TemplateChildNode, @@ -125,12 +126,15 @@ export class TransformContext { } 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 @@ -214,7 +218,8 @@ export function transform( type: IRNodeTypes.ROOT, node, source: node.source, - template: [], + template: new Map(), + templateIndexMap: new Map(), component: new Set(), directive: new Set(), block: newBlock(node), diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index 05153e729af..1919ccd2a2b 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -250,7 +250,7 @@ function transformNativeElement( } if (singleRoot) { - context.ir.rootTemplateIndex = context.ir.template.length + context.ir.rootTemplateIndex = context.ir.template.size } if ( diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index b241458dba7..4c43efd4e7f 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -348,3 +348,7 @@ export { vModelSelectInit, vModelSetSelected, } from './directives/vModel' +/** + * @internal + */ +export { svgNS, mathmlNS } from './nodeOps' diff --git a/packages/runtime-vapor/__tests__/dom/mathML.spec.ts b/packages/runtime-vapor/__tests__/dom/mathML.spec.ts new file mode 100644 index 00000000000..eed0b8218a4 --- /dev/null +++ b/packages/runtime-vapor/__tests__/dom/mathML.spec.ts @@ -0,0 +1,84 @@ +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( + ` + + + + x + 2 + + + + y + + +
+ +
+
+
`, + 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( + '
', + 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') + }) +}) diff --git a/packages/runtime-vapor/__tests__/dom/svg.spec.ts b/packages/runtime-vapor/__tests__/dom/svg.spec.ts new file mode 100644 index 00000000000..850a7a9c928 --- /dev/null +++ b/packages/runtime-vapor/__tests__/dom/svg.spec.ts @@ -0,0 +1,73 @@ +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( + `
+ + +
+ + +
+
+
`, + 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( + '
hi
', + 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') + }) +}) diff --git a/packages/runtime-vapor/src/dom/template.ts b/packages/runtime-vapor/src/dom/template.ts index b78ca4e52cf..66b9ea7d2de 100644 --- a/packages/runtime-vapor/src/dom/template.ts +++ b/packages/runtime-vapor/src/dom/template.ts @@ -1,10 +1,14 @@ +import { mathmlNS, svgNS } from '@vue/runtime-dom' import { adoptTemplate, currentHydrationNode, isHydrating } from './hydration' import { child, createTextNode } from './node' +import { type Namespace, Namespaces } from '@vue/shared' let t: HTMLTemplateElement +let st: HTMLTemplateElement +let mt: 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) { @@ -19,9 +23,19 @@ export function template(html: string, root?: boolean) { return createTextNode(html) } if (!node) { - t = t || document.createElement('template') - t.innerHTML = html - node = child(t.content) + if (!ns) { + t = t || document.createElement('template') + t.innerHTML = html + node = child(t.content) + } else if (ns === Namespaces.SVG) { + st = st || document.createElementNS(svgNS, 'template') + st.innerHTML = html + node = child(st) + } else { + mt = mt || document.createElementNS(mathmlNS, 'template') + mt.innerHTML = html + node = child(mt) + } } const ret = node.cloneNode(true) if (root) (ret as any).$root = true diff --git a/packages/shared/src/domNamespace.ts b/packages/shared/src/domNamespace.ts new file mode 100644 index 00000000000..42ef2380872 --- /dev/null +++ b/packages/shared/src/domNamespace.ts @@ -0,0 +1,10 @@ +// 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, +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 0c38d640ba0..519546a4685 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -8,6 +8,7 @@ export * from './codeframe' export * from './normalizeProp' export * from './domTagConfig' export * from './domAttrConfig' +export * from './domNamespace' export * from './escapeHtml' export * from './looseEqual' export * from './toDisplayString'