From: edison Date: Mon, 10 Nov 2025 01:25:08 +0000 (+0800) Subject: feat(runtime-vapor): support svg and MathML (#13703) X-Git-Tag: v3.6.0-alpha.4~9 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=f0d0cfd1d48a61ea5f5e094f6546272a758363c9;p=thirdparty%2Fvuejs%2Fcore.git feat(runtime-vapor): support svg and MathML (#13703) --- diff --git a/packages/compiler-core/__tests__/parse.spec.ts b/packages/compiler-core/__tests__/parse.spec.ts index cdc2b09fd4..306d8089ff 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 a2525e0cab..6ef41b48b0 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 bae13372a9..929a86e4bf 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 9983071609..4c2b2f7707 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 2d85289fc6..d844877408 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 7418b8e33f..e02b8c7b87 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 7da13bf534..4106bd5af3 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 ba05499a91..c4329e5482 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, @@ -21,6 +20,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 cad1ee8102..5cf9c736a8 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 8d9df60dfa..517c282a2c 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 > checkbox with static indeterminate 1`] = ` "import { setProp as _setProp, template as _template } from 'vue'; const t0 = _template("", true) @@ -448,6 +458,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 4bbf1884d9..6fecc45962 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, true)) + return n0 +}" +`; + exports[`compiler v-bind > :innerHTML 1`] = ` "import { setHtml as _setHtml, renderEffect as _renderEffect, template as _template } from 'vue'; const t0 = _template("
", 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("", 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("
", true) diff --git a/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts index 66768508f6..a4a833726c 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) }) @@ -600,7 +600,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) }) @@ -1018,7 +1018,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 }] }, @@ -1037,4 +1041,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 39b2bdcc8c..ffd0a6da62 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 4a1d011c17..a0ffe4fc4e 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/transformText.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformText.spec.ts index 1c929f0f20..fa51d085eb 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformText.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformText.spec.ts @@ -51,8 +51,8 @@ describe('compiler: text transform', () => { it('escapes raw static text when generating the template string', () => { const { ir } = compileWithTextTransform('<script>') - expect(ir.template).toContain('<script>') - expect(ir.template).not.toContain('