feat(runtime-vapor): support svg and MathML

This commit is contained in:
daiwei 2025-07-25 16:10:29 +08:00
parent f70f9d1a6b
commit 40b1e1e5de
5 changed files with 37 additions and 12 deletions

View File

@ -7,15 +7,15 @@ import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils'
export function genTemplates( export function genTemplates(
templates: string[], templates: string[],
rootIndex: number | undefined, rootIndex: number | undefined,
{ helper }: CodegenContext, { helper, ir: { templateNS } }: CodegenContext,
): string { ): string {
return templates return templates
.map( .map((template, i) => {
(template, i) => const ns = templateNS.get(template)
`const t${i} = ${helper('template')}(${JSON.stringify( return `const t${i} = ${helper('template')}(${JSON.stringify(
template, template,
)}${i === rootIndex ? ', true' : ''})\n`, )}${i === rootIndex ? ', true' : ns ? ', false' : ''}${ns ? `, ${ns}` : ''})\n`
) })
.join('') .join('')
} }

View File

@ -60,6 +60,7 @@ export interface RootIRNode {
node: RootNode node: RootNode
source: string source: string
template: string[] template: string[]
templateNS: Map<string, number>
rootTemplateIndex?: number rootTemplateIndex?: number
component: Set<string> component: Set<string>
directive: Set<string> directive: Set<string>

View File

@ -6,6 +6,7 @@ import {
type ElementNode, type ElementNode,
ElementTypes, ElementTypes,
NodeTypes, NodeTypes,
type PlainElementNode,
type RootNode, type RootNode,
type SimpleExpressionNode, type SimpleExpressionNode,
type TemplateChildNode, type TemplateChildNode,
@ -73,6 +74,7 @@ export class TransformContext<T extends AllNode = AllNode> {
> >
template: string = '' template: string = ''
templateNS: Map<string, number> = new Map<string, number>()
childrenTemplate: (string | null)[] = [] childrenTemplate: (string | null)[] = []
dynamic: IRDynamicInfo = this.ir.block.dynamic dynamic: IRDynamicInfo = this.ir.block.dynamic
@ -98,10 +100,12 @@ export class TransformContext<T extends AllNode = AllNode> {
} }
enterBlock(ir: BlockIRNode, isVFor: boolean = false): () => void { enterBlock(ir: BlockIRNode, isVFor: boolean = false): () => void {
const { block, template, dynamic, childrenTemplate, slots } = this const { block, template, templateNS, dynamic, childrenTemplate, slots } =
this
this.block = ir this.block = ir
this.dynamic = ir.dynamic this.dynamic = ir.dynamic
this.template = '' this.template = ''
this.templateNS = new Map<string, number>()
this.childrenTemplate = [] this.childrenTemplate = []
this.slots = [] this.slots = []
isVFor && this.inVFor++ isVFor && this.inVFor++
@ -110,6 +114,7 @@ export class TransformContext<T extends AllNode = AllNode> {
this.registerTemplate() this.registerTemplate()
this.block = block this.block = block
this.template = template this.template = template
this.templateNS = templateNS
this.dynamic = dynamic this.dynamic = dynamic
this.childrenTemplate = childrenTemplate this.childrenTemplate = childrenTemplate
this.slots = slots this.slots = slots
@ -130,6 +135,7 @@ export class TransformContext<T extends AllNode = AllNode> {
) )
if (existing !== -1) return existing if (existing !== -1) return existing
this.ir.template.push(content) this.ir.template.push(content)
this.ir.templateNS.set(content, (this.node as PlainElementNode).ns)
return this.ir.template.length - 1 return this.ir.template.length - 1
} }
registerTemplate(): number { registerTemplate(): number {
@ -215,6 +221,7 @@ export function transform(
node, node,
source: node.source, source: node.source,
template: [], template: [],
templateNS: new Map<string, number>(),
component: new Set(), component: new Set(),
directive: new Set(), directive: new Set(),
block: newBlock(node), block: newBlock(node),

View File

@ -348,3 +348,7 @@ export {
vModelSelectInit, vModelSelectInit,
vModelSetSelected, vModelSetSelected,
} from './directives/vModel' } from './directives/vModel'
/**
* @internal
*/
export { svgNS, mathmlNS } from './nodeOps'

View File

@ -1,10 +1,13 @@
import { mathmlNS, svgNS } from '@vue/runtime-dom'
import { adoptTemplate, currentHydrationNode, isHydrating } from './hydration' import { adoptTemplate, currentHydrationNode, isHydrating } from './hydration'
import { child, createTextNode } from './node' import { child, createTextNode } from './node'
let t: HTMLTemplateElement let t: HTMLTemplateElement
let st: HTMLTemplateElement
let mt: HTMLTemplateElement
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export function template(html: string, root?: boolean) { export function template(html: string, root?: boolean, ns?: number) {
let node: Node let node: Node
return (): Node & { $root?: true } => { return (): Node & { $root?: true } => {
if (isHydrating) { if (isHydrating) {
@ -19,9 +22,19 @@ export function template(html: string, root?: boolean) {
return createTextNode(html) return createTextNode(html)
} }
if (!node) { if (!node) {
t = t || document.createElement('template') if (!ns) {
t.innerHTML = html t = t || document.createElement('template')
node = child(t.content) t.innerHTML = html
node = child(t.content)
} else if (ns === 1) {
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) const ret = node.cloneNode(true)
if (root) (ret as any).$root = true if (root) (ret as any).$root = true