Skip to content

feat(runtime-vapor): support svg and MathML #13703

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: minor
Choose a base branch
from
Open
2 changes: 1 addition & 1 deletion packages/compiler-core/__tests__/parse.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import {
type ElementNode,
ElementTypes,
type InterpolationNode,
Namespaces,
NodeTypes,
type Position,
type TextNode,
} from '../src/ast'

import { baseParse } from '../src/parser'
import type { Program } from '@babel/types'
import { Namespaces } from '@vue/shared'

describe('compiler: parse', () => {
describe('Text', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler-core/__tests__/testUtils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import {
type ElementNode,
ElementTypes,
Namespaces,
NodeTypes,
type Property,
type SimpleExpressionNode,
type VNodeCall,
locStub,
} from '../src'
import {
Namespaces,
PatchFlagNames,
type PatchFlags,
type ShapeFlags,
Expand Down
12 changes: 1 addition & 11 deletions packages/compiler-core/src/ast.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand Down
9 changes: 2 additions & 7 deletions packages/compiler-core/src/options.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler-core/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
type ElementNode,
ElementTypes,
type ForParseResult,
Namespaces,
NodeTypes,
type RootNode,
type SimpleExpressionNode,
Expand All @@ -14,6 +13,7 @@ import {
createRoot,
createSimpleExpression,
} from './ast'
import { Namespaces } from '@vue/shared'
import type { ParserOptions } from './options'
import Tokenizer, {
CharCodes,
Expand Down
24 changes: 23 additions & 1 deletion packages/compiler-dom/__tests__/parse.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down
16 changes: 11 additions & 5 deletions packages/compiler-dom/src/parserOptions.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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 (
Expand Down Expand Up @@ -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
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler-dom/src/transforms/stringifyStatic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
ElementTypes,
type ExpressionNode,
type HoistTransform,
Namespaces,
NodeTypes,
type PlainElementNode,
type SimpleExpressionNode,
Expand All @@ -20,6 +19,7 @@ import {
isStaticArgOf,
} from '@vue/compiler-core'
import {
Namespaces,
escapeHtml,
isArray,
isBooleanAttr,
Expand Down
10 changes: 8 additions & 2 deletions packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
type ExpressionNode,
type FunctionExpression,
type JSChildNode,
Namespaces,
type NodeTransform,
NodeTypes,
RESOLVE_DYNAMIC_COMPONENT,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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 > component > cache v-on expression with unique handler name 1`] = `
"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';

Expand Down Expand Up @@ -407,6 +417,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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})

Expand All @@ -591,7 +591,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)
})

Expand Down Expand Up @@ -937,7 +937,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 }] },
Expand All @@ -956,4 +960,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)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading