]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(runtime-vapor): support svg and MathML (#13703)
authoredison <daiwei521@126.com>
Mon, 10 Nov 2025 01:25:08 +0000 (09:25 +0800)
committerGitHub <noreply@github.com>
Mon, 10 Nov 2025 01:25:08 +0000 (09:25 +0800)
36 files changed:
packages/compiler-core/__tests__/parse.spec.ts
packages/compiler-core/__tests__/testUtils.ts
packages/compiler-core/src/ast.ts
packages/compiler-core/src/options.ts
packages/compiler-core/src/parser.ts
packages/compiler-dom/__tests__/parse.spec.ts
packages/compiler-dom/src/parserOptions.ts
packages/compiler-dom/src/transforms/stringifyStatic.ts
packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts
packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts
packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts
packages/compiler-vapor/__tests__/transforms/transformText.spec.ts
packages/compiler-vapor/__tests__/transforms/vBind.spec.ts
packages/compiler-vapor/__tests__/transforms/vFor.spec.ts
packages/compiler-vapor/__tests__/transforms/vHtml.spec.ts
packages/compiler-vapor/__tests__/transforms/vIf.spec.ts
packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts
packages/compiler-vapor/__tests__/transforms/vText.spec.ts
packages/compiler-vapor/src/generators/prop.ts
packages/compiler-vapor/src/generators/template.ts
packages/compiler-vapor/src/ir/index.ts
packages/compiler-vapor/src/transform.ts
packages/compiler-vapor/src/transforms/transformElement.ts
packages/runtime-dom/__tests__/patchAttrs.spec.ts
packages/runtime-dom/src/components/TransitionGroup.ts
packages/runtime-dom/src/index.ts
packages/runtime-vapor/__tests__/dom/mathML.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/dom/prop.spec.ts
packages/runtime-vapor/__tests__/dom/svg.spec.ts [new file with mode: 0644]
packages/runtime-vapor/src/dom/prop.ts
packages/runtime-vapor/src/dom/template.ts
packages/shared/src/domNamespace.ts [new file with mode: 0644]
packages/shared/src/index.ts

index cdc2b09fd48d12d6ba9c7a91e96fb688efbff787..306d8089ff2e9c5c841bfcd41e7f370f6ed04894 100644 (file)
@@ -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', () => {
index a2525e0cab985207d333edd8bad0552154832904..6ef41b48b0719ae3a03eb8d9c75e345160797464 100644 (file)
@@ -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,
index bae13372a98be3d205d15cc1c045edd0b0220197..929a86e4bf392bc4171c1ec405df11316e9f1f0f 100644 (file)
@@ -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,
index 9983071609eaad3d1337543438771eed7a8d7c61..4c2b2f7707062de06c618bfea5dbd0a7465711de 100644 (file)
@@ -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,
index 2d85289fc6842afe6c9cfaa97b194e86eb3416a6..d844877408b7cd2faac57f9b3f3c2c993f4e8f38 100644 (file)
@@ -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,
index 7418b8e33fbfbd68569f4a97cfda987ece0521ea..e02b8c7b87ab6c61674bf2cc3ca70586153923e2 100644 (file)
@@ -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('<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
@@ -500,6 +511,17 @@ describe('DOM parser', () => {
       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,
index 7da13bf534d4d627e535f456859f991e799f8a35..4106bd5af312796162ec1fc4a59f3465942610c7 100644 (file)
@@ -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
       }
     }
index ba05499a914b29c2e20ccc32afdf93a2f2d32ce4..c4329e5482c75c66ac6aaa6f48f5dd1e38a663a6 100644 (file)
@@ -9,7 +9,6 @@ import {
   ElementTypes,
   type ExpressionNode,
   type HoistTransform,
-  Namespaces,
   NodeTypes,
   type PlainElementNode,
   type SimpleExpressionNode,
@@ -21,6 +20,7 @@ import {
   isStaticArgOf,
 } from '@vue/compiler-core'
 import {
+  Namespaces,
   escapeHtml,
   isArray,
   isBooleanAttr,
index cad1ee8102897beab4c8990b99b43366c5d42655..5cf9c736a8458d793e290df0168b12a5fcc7063b 100644 (file)
@@ -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,
index 8d9df60dfa131cda9cad23b18cfa14affa1d2e3d..517c282a2cfffb12f8b881a64f329cdcc1d7b47f 100644 (file)
@@ -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("<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)
@@ -448,6 +458,16 @@ export function render(_ctx) {
 }"
 `;
 
+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)
index 4bbf1884d94128ac79bd61452d3827a9b50bc8d9..6fecc45962a615e6b2f3afacf82b9c1567a9cac2 100644 (file)
@@ -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("<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)
@@ -631,6 +642,17 @@ export function render(_ctx) {
 }"
 `;
 
+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)
index 66768508f67afcb509311c973a53458235b2bd52..a4a833726cb075f2bbbff799ac9c13d73c73ae16 100644 (file)
@@ -579,7 +579,7 @@ describe('compiler: element transform', () => {
     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)
   })
 
@@ -600,7 +600,7 @@ describe('compiler: element transform', () => {
     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)
   })
 
@@ -1018,7 +1018,11 @@ describe('compiler: element transform', () => {
       <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 }] },
@@ -1037,4 +1041,26 @@ describe('compiler: element transform', () => {
     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)
+  })
 })
index 39b2bdcc8c336fdbe2042c7f4a60a36c1dc5b4e1..ffd0a6da628e4b6c5359f66701817e5dd6a60947 100644 (file)
@@ -155,7 +155,7 @@ describe('compiler: transform <slot> outlets', () => {
   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,
@@ -175,7 +175,7 @@ describe('compiler: transform <slot> outlets', () => {
       `<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,
@@ -195,7 +195,7 @@ describe('compiler: transform <slot> outlets', () => {
       `<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,
@@ -216,7 +216,7 @@ describe('compiler: transform <slot> outlets', () => {
       `<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,
index 4a1d011c178f45c1b7bec81f923913cb1e2a8d5f..a0ffe4fc4e3ba79e46090cbc486defabd42be5cf 100644 (file)
@@ -30,7 +30,7 @@ describe('compiler: template ref transform', () => {
       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,
@@ -66,7 +66,7 @@ describe('compiler: template ref transform', () => {
       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,
@@ -104,7 +104,7 @@ describe('compiler: template ref transform', () => {
       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,
index 1c929f0f206296aab6f0548f01c25a771172c9ef..fa51d085eb7c3d5f3b41b6eea441625dc2e31e28 100644 (file)
@@ -51,8 +51,8 @@ describe('compiler: text transform', () => {
 
   it('escapes raw static text when generating the template string', () => {
     const { ir } = compileWithTextTransform('<code>&lt;script&gt;</code>')
-    expect(ir.template).toContain('<code>&lt;script&gt;</code>')
-    expect(ir.template).not.toContain('<code><script></code>')
+    expect([...ir.template.keys()]).toContain('<code>&lt;script&gt;</code>')
+    expect([...ir.template.keys()]).not.toContain('<code><script></code>')
   })
 
   test('constant text', () => {
index 9c07b9de90b47002fb9bbba6df014d210e031c5a..f14cf29499ee5492f66b8b3c5efe6cf6a296b6a2 100644 (file)
@@ -23,7 +23,7 @@ describe('compiler v-bind', () => {
       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)
@@ -241,7 +241,7 @@ describe('compiler v-bind', () => {
         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>'))
@@ -656,6 +656,22 @@ describe('compiler v-bind', () => {
     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()
index f79bec89ddaed2501f6c6cf5807252d14c1bccaf..d57dfe90b8de4264f384f64a03e2740b5b18dbc8 100644 (file)
@@ -32,7 +32,7 @@ describe('compiler: v-for', () => {
 
     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,
@@ -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(['<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,
index 1b3b4963812f67cc5998ee7421704f0189c626bd..d507f7ea0f3c9e6f8cfd31b1b00a7f241484410f 100644 (file)
@@ -78,7 +78,7 @@ describe('v-html', () => {
     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([
index 6a8148b7c187c058253e61f3e7ce51194c969021..cfac122885efed55b081deef6ae49404a765b89a 100644 (file)
@@ -32,7 +32,7 @@ describe('compiler: v-if', () => {
 
     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({
@@ -68,7 +68,11 @@ describe('compiler: v-if', () => {
     )
     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([
@@ -103,7 +107,7 @@ describe('compiler: v-if', () => {
       `<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])
   })
 
@@ -113,7 +117,7 @@ describe('compiler: v-if', () => {
   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)
@@ -146,7 +150,7 @@ describe('compiler: v-if', () => {
       `<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,
@@ -185,7 +189,7 @@ describe('compiler: v-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({
@@ -239,7 +243,7 @@ describe('compiler: v-if', () => {
       <div v-text="text" />
     `)
     expect(code).matchSnapshot()
-    expect(ir.template).toEqual([
+    expect([...ir.template.keys()]).toEqual([
       '<div></div>',
       '<!--foo-->',
       '<p></p>',
index f2e94814c43a437082eeb541b3ad8cc7266fb834..64d1b4ac4a446ad38827e4b3187442aacd181fa2 100644 (file)
@@ -35,7 +35,7 @@ describe('compiler: transform slot', () => {
     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,
@@ -163,7 +163,7 @@ describe('compiler: transform slot', () => {
     )
     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,
index d1cfd18f96e60fa081ab7ff2f183526d0ba4503d..e1f5684b825e6d8183f3d36d4745d31064ccc383 100644 (file)
@@ -80,7 +80,7 @@ describe('v-text', () => {
     ])
 
     // children should have been removed
-    expect(ir.template).toEqual(['<div> </div>'])
+    expect([...ir.template.keys()]).toEqual(['<div> </div>'])
 
     expect(ir.block.effect).toMatchObject([
       {
index e486cffeb56940fe66733bcd02aad4417c66d2ea..6b679da99f224093037f8293aaee1d350f3bf2a7 100644 (file)
@@ -23,6 +23,7 @@ import {
 import {
   canSetValueDirectly,
   capitalize,
+  extend,
   isSVGTag,
   shouldSetAsAttr,
   toHandlerKey,
@@ -31,6 +32,7 @@ import {
 export type HelperConfig = {
   name: VaporHelper
   needKey?: boolean
+  isSVG?: boolean
   acceptRoot?: boolean
 }
 
@@ -44,7 +46,6 @@ const helpers = {
   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
@@ -66,6 +67,7 @@ export function genSetProp(
       `n${oper.element}`,
       resolvedHelper.needKey ? genExpression(key, context) : false,
       propValue,
+      resolvedHelper.isSVG && 'true',
     ),
   ]
 }
@@ -76,6 +78,7 @@ export function genDynamicProps(
   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
@@ -90,6 +93,7 @@ export function genDynamicProps(
       `n${oper.element}`,
       genMulti(DELIMITERS_ARRAY, ...values),
       oper.root && 'true',
+      isSVG && 'true',
     ),
   ]
 }
@@ -170,6 +174,13 @@ function getRuntimeHelper(
   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
@@ -178,24 +189,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('-')) {
index 1bf99ec3834a3a86d39cc81665257195d07d863c..adb7cef1b623624773001b91fcfcbc9a5d64eb61 100644 (file)
@@ -9,18 +9,21 @@ import { genOperationWithInsertionState } from './operation'
 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(
index 15cf85ae10e97fc82235b4414a786ac4825055f7..177997d68b178d80088bcaad8f3ad9e8319bbeae 100644 (file)
@@ -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'
 
@@ -61,7 +61,8 @@ export interface RootIRNode {
   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>
@@ -116,6 +117,7 @@ export interface SetDynamicPropsIRNode extends BaseIRNode {
   element: number
   props: IRProps[]
   root: boolean
+  tag: string
 }
 
 export interface SetDynamicEventsIRNode extends BaseIRNode {
index d5cec956fa68a6370caf958312f71959b6583cc5..98db54d3153ab7655262dc28d169cac0e75fe10e 100644 (file)
@@ -6,6 +6,7 @@ import {
   type ElementNode,
   ElementTypes,
   NodeTypes,
+  type PlainElementNode,
   type RootNode,
   type SimpleExpressionNode,
   type TemplateChildNode,
@@ -154,12 +155,15 @@ export class TransformContext<T extends AllNode = AllNode> {
   }
 
   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
@@ -243,7 +247,8 @@ export function transform(
     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),
index facffadff109422e2997d920e339bad747c7b720..641d1472acd38acd67c2de6de54d39b46a808e1e 100644 (file)
@@ -226,6 +226,7 @@ function transformNativeElement(
         element: context.reference(),
         props: dynamicArgs,
         root: singleRoot,
+        tag,
       },
       getEffectIndex,
     )
@@ -264,7 +265,7 @@ function transformNativeElement(
   }
 
   if (singleRoot) {
-    context.ir.rootTemplateIndex = context.ir.template.length
+    context.ir.rootTemplateIndex = context.ir.template.size
   }
 
   if (
index d181a1b038b5b2f269f0f7d5cd86393a0342d28a..58dcb8b727853fe30cf77932d98bf8075eaa748a 100644 (file)
@@ -10,7 +10,7 @@ describe('runtime-dom: attrs patching', () => {
     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)
index b7ba56f99fcc83c7e03d0ed8e37b3eb209e12ff2..7556c54049c3c15fbeb7baff002d29917fc57793 100644 (file)
@@ -37,7 +37,7 @@ interface Position {
 
 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'> & {
index c4518b00291d268752ceba871571223984e63373..f275d65a82d5de3edcc707918ea25adcd9fbec82 100644 (file)
@@ -347,6 +347,14 @@ export {
   vModelSelectInit,
   vModelSetSelected,
 } from './directives/vModel'
+/**
+ * @internal
+ */
+export { svgNS } from './nodeOps'
+/**
+ * @internal
+ */
+export { xlinkNS } from './modules/attrs'
 /**
  * @internal
  */
@@ -354,8 +362,6 @@ export {
   resolveTransitionProps,
   TransitionPropsValidators,
   forceReflow,
-  addTransitionClass,
-  removeTransitionClass,
   type ElementWithTransition,
 } from './components/Transition'
 /**
@@ -364,7 +370,6 @@ export {
 export {
   hasCSSTransform,
   callPendingCbs,
-  moveCbKey,
   handleMovedChildren,
   baseApplyTranslation,
 } from './components/TransitionGroup'
diff --git a/packages/runtime-vapor/__tests__/dom/mathML.spec.ts b/packages/runtime-vapor/__tests__/dom/mathML.spec.ts
new file mode 100644 (file)
index 0000000..eed0b82
--- /dev/null
@@ -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(
+          `<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')
+  })
+})
index f185e0e067bf4de1f269f289f026d45a0c5c561a..dd29281bac8e1d64e2ed9bf1caecaffd3e439f04 100644 (file)
@@ -14,7 +14,7 @@ import {
 } 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,
@@ -329,8 +329,9 @@ describe('patchProp', () => {
       key: string,
       value: any,
       el = element.cloneNode(true) as HTMLElement,
+      isSVG: boolean = false,
     ) {
-      _setDynamicProp(el, key, value)
+      _setDynamicProp(el, key, value, isSVG)
       return el
     }
 
@@ -381,7 +382,40 @@ describe('patchProp', () => {
       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', () => {
diff --git a/packages/runtime-vapor/__tests__/dom/svg.spec.ts b/packages/runtime-vapor/__tests__/dom/svg.spec.ts
new file mode 100644 (file)
index 0000000..850a7a9
--- /dev/null
@@ -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(
+          `<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')
+  })
+})
index b104b20900d7687c46b7d654954e33273f7eedc7..6d41a7c1bce2b3406352016bf698e4e9e24a41c4 100644 (file)
@@ -29,6 +29,7 @@ import {
   vShowHidden,
   warn,
   warnPropMismatch,
+  xlinkNS,
 } from '@vue/runtime-dom'
 import {
   type VaporComponentInstance,
@@ -59,7 +60,12 @@ export function setProp(el: any, key: string, value: any): void {
   }
 }
 
-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
   }
@@ -85,10 +91,18 @@ export function setAttr(el: any, key: string, value: any): void {
 
   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)
+      }
     }
   }
 }
@@ -152,7 +166,11 @@ export function setDOMProp(
   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 {
@@ -167,7 +185,11 @@ export function setClass(el: TargetElement, value: any): void {
     }
 
     if (value !== el.$cls) {
-      el.className = el.$cls = value
+      if (isSVG) {
+        el.setAttribute('class', (el.$cls = value))
+      } else {
+        el.className = el.$cls = value
+      }
     }
   }
 }
@@ -405,21 +427,27 @@ function setHtmlToBlock(block: Block, value: any): void {
   }
 }
 
-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)
   }
 }
 
@@ -430,12 +458,11 @@ export function setDynamicProp(
   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)) {
@@ -458,7 +485,7 @@ export function setDynamicProp(
       setDOMProp(el, key, value, forceHydrate)
     }
   } else {
-    setAttr(el, key, value)
+    setAttr(el, key, value, isSVG)
   }
   return value
 }
index 5db46476f71415da96139410e2b65257a8fe3773..23c56e0ca14f294dd2e3aeb88bb50dd0b954b05f 100644 (file)
@@ -1,10 +1,11 @@
 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) {
@@ -20,9 +21,15 @@ export function template(html: string, root?: boolean) {
       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
diff --git a/packages/shared/src/domNamespace.ts b/packages/shared/src/domNamespace.ts
new file mode 100644 (file)
index 0000000..42ef238
--- /dev/null
@@ -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,
+}
index 0c38d640ba0982f7a8439279624ca9f71018042a..519546a468571c3c32aabf7898175c146a9ce976 100644 (file)
@@ -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'