feat(compiler-core): support specifying root namespace when parsing

This commit is contained in:
Evan You 2023-11-19 11:20:05 +08:00
parent a1b10a21ac
commit 40f72d5e50
6 changed files with 92 additions and 56 deletions

View File

@ -16,12 +16,13 @@ import { PropsExpression } from './transforms/transformElement'
import { ImportItem, TransformContext } from './transform'
// Vue template is a platform-agnostic superset of HTML (syntax only).
// More namespaces like SVG and MathML are declared by platform specific
// compilers.
// More namespaces can be declared by platform specific compilers.
export type Namespace = number
export const enum Namespaces {
HTML
HTML,
SVG,
MATH_ML
}
export const enum NodeTypes {

View File

@ -1,4 +1,10 @@
import { ElementNode, Namespace, TemplateChildNode, ParentNode } from './ast'
import {
ElementNode,
Namespace,
TemplateChildNode,
ParentNode,
Namespaces
} from './ast'
import { CompilerError } from './errors'
import {
NodeTransform,
@ -16,7 +22,24 @@ export interface ErrorHandlingOptions {
export interface ParserOptions
extends ErrorHandlingOptions,
CompilerCompatOptions {
/**
* Base mode is platform agnostic and only parses HTML-like template syntax,
* treating all tags the same way. Specific tag parsing behavior can be
* configured by higher-level compilers.
*
* HTML mode adds additional logic for handling special parsing behavior in
* `<script>`, `<style>`,`<title>` and `<html>`, plus SVG and MathML
* namespaces. The logic is handled inside compiler-core for efficiency.
*
* SFC mode treats content of all root-level tags except `<template>` as plain
* text.
*/
parseMode?: 'base' | 'html' | 'sfc'
/**
* Specify the root namepsace to use when parsing a tempalte.
* Defaults to `Namepsaces.HTML` (0).
*/
ns?: Namespaces
/**
* e.g. platform native elements, e.g. `<div>` for browsers
*/
@ -40,7 +63,11 @@ export interface ParserOptions
/**
* Get tag namespace
*/
getNamespace?: (tag: string, parent: ElementNode | undefined) => Namespace
getNamespace?: (
tag: string,
parent: ElementNode | undefined,
rootNamespace: Namespace
) => Namespace
/**
* @default ['{{', '}}']
*/

View File

@ -40,6 +40,7 @@ type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> &
export const defaultParserOptions: MergedParserOptions = {
parseMode: 'base',
ns: Namespaces.HTML,
delimiters: [`{{`, `}}`],
getNamespace: () => Namespaces.HTML,
isVoidTag: NO,
@ -107,7 +108,7 @@ const tokenizer = new Tokenizer(stack, {
currentElement = {
type: NodeTypes.ELEMENT,
tag: name,
ns: currentOptions.getNamespace(name, stack[0]),
ns: currentOptions.getNamespace(name, stack[0], currentOptions.ns),
tagType: ElementTypes.ELEMENT, // will be refined on tag close
props: [],
children: [],

View File

@ -6,9 +6,10 @@ import {
ElementTypes,
InterpolationNode,
AttributeNode,
ConstantTypes
ConstantTypes,
Namespaces
} from '@vue/compiler-core'
import { parserOptions, DOMNamespaces } from '../src/parserOptions'
import { parserOptions } from '../src/parserOptions'
describe('DOM parser', () => {
describe('Text', () => {
@ -264,7 +265,7 @@ describe('DOM parser', () => {
expect(element).toStrictEqual({
type: NodeTypes.ELEMENT,
ns: DOMNamespaces.HTML,
ns: Namespaces.HTML,
tag: 'img',
tagType: ElementTypes.ELEMENT,
props: [],
@ -324,21 +325,21 @@ describe('DOM parser', () => {
const ast = parse('<html>test</html>', parserOptions)
const element = ast.children[0] as ElementNode
expect(element.ns).toBe(DOMNamespaces.HTML)
expect(element.ns).toBe(Namespaces.HTML)
})
test('SVG namespace', () => {
const ast = parse('<svg>test</svg>', parserOptions)
const element = ast.children[0] as ElementNode
expect(element.ns).toBe(DOMNamespaces.SVG)
expect(element.ns).toBe(Namespaces.SVG)
})
test('MATH_ML namespace', () => {
const ast = parse('<math>test</math>', parserOptions)
const element = ast.children[0] as ElementNode
expect(element.ns).toBe(DOMNamespaces.MATH_ML)
expect(element.ns).toBe(Namespaces.MATH_ML)
})
test('SVG in MATH_ML namespace', () => {
@ -350,8 +351,8 @@ describe('DOM parser', () => {
const elementAnnotation = elementMath.children[0] as ElementNode
const elementSvg = elementAnnotation.children[0] as ElementNode
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
expect(elementMath.ns).toBe(Namespaces.MATH_ML)
expect(elementSvg.ns).toBe(Namespaces.SVG)
})
test('html text/html in MATH_ML namespace', () => {
@ -364,8 +365,8 @@ describe('DOM parser', () => {
const elementAnnotation = elementMath.children[0] as ElementNode
const element = elementAnnotation.children[0] as ElementNode
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
expect(element.ns).toBe(DOMNamespaces.HTML)
expect(elementMath.ns).toBe(Namespaces.MATH_ML)
expect(element.ns).toBe(Namespaces.HTML)
})
test('html application/xhtml+xml in MATH_ML namespace', () => {
@ -377,8 +378,8 @@ describe('DOM parser', () => {
const elementAnnotation = elementMath.children[0] as ElementNode
const element = elementAnnotation.children[0] as ElementNode
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
expect(element.ns).toBe(DOMNamespaces.HTML)
expect(elementMath.ns).toBe(Namespaces.MATH_ML)
expect(element.ns).toBe(Namespaces.HTML)
})
test('mtext malignmark in MATH_ML namespace', () => {
@ -390,8 +391,8 @@ describe('DOM parser', () => {
const elementText = elementMath.children[0] as ElementNode
const element = elementText.children[0] as ElementNode
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
expect(element.ns).toBe(DOMNamespaces.MATH_ML)
expect(elementMath.ns).toBe(Namespaces.MATH_ML)
expect(element.ns).toBe(Namespaces.MATH_ML)
})
test('mtext and not malignmark tag in MATH_ML namespace', () => {
@ -400,8 +401,8 @@ describe('DOM parser', () => {
const elementText = elementMath.children[0] as ElementNode
const element = elementText.children[0] as ElementNode
expect(elementMath.ns).toBe(DOMNamespaces.MATH_ML)
expect(element.ns).toBe(DOMNamespaces.HTML)
expect(elementMath.ns).toBe(Namespaces.MATH_ML)
expect(element.ns).toBe(Namespaces.HTML)
})
test('foreignObject tag in SVG namespace', () => {
@ -413,8 +414,8 @@ describe('DOM parser', () => {
const elementForeignObject = elementSvg.children[0] as ElementNode
const element = elementForeignObject.children[0] as ElementNode
expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
expect(element.ns).toBe(DOMNamespaces.HTML)
expect(elementSvg.ns).toBe(Namespaces.SVG)
expect(element.ns).toBe(Namespaces.HTML)
})
test('desc tag in SVG namespace', () => {
@ -423,8 +424,8 @@ describe('DOM parser', () => {
const elementDesc = elementSvg.children[0] as ElementNode
const element = elementDesc.children[0] as ElementNode
expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
expect(element.ns).toBe(DOMNamespaces.HTML)
expect(elementSvg.ns).toBe(Namespaces.SVG)
expect(element.ns).toBe(Namespaces.HTML)
})
test('title tag in SVG namespace', () => {
@ -433,8 +434,8 @@ describe('DOM parser', () => {
const elementTitle = elementSvg.children[0] as ElementNode
const element = elementTitle.children[0] as ElementNode
expect(elementSvg.ns).toBe(DOMNamespaces.SVG)
expect(element.ns).toBe(DOMNamespaces.HTML)
expect(elementSvg.ns).toBe(Namespaces.SVG)
expect(element.ns).toBe(Namespaces.HTML)
})
test('SVG in HTML namespace', () => {
@ -442,8 +443,8 @@ describe('DOM parser', () => {
const elementHtml = ast.children[0] as ElementNode
const element = elementHtml.children[0] as ElementNode
expect(elementHtml.ns).toBe(DOMNamespaces.HTML)
expect(element.ns).toBe(DOMNamespaces.SVG)
expect(elementHtml.ns).toBe(Namespaces.HTML)
expect(element.ns).toBe(Namespaces.SVG)
})
test('MATH in HTML namespace', () => {
@ -451,8 +452,20 @@ describe('DOM parser', () => {
const elementHtml = ast.children[0] as ElementNode
const element = elementHtml.children[0] as ElementNode
expect(elementHtml.ns).toBe(DOMNamespaces.HTML)
expect(element.ns).toBe(DOMNamespaces.MATH_ML)
expect(elementHtml.ns).toBe(Namespaces.HTML)
expect(element.ns).toBe(Namespaces.MATH_ML)
})
test('root ns', () => {
const ast = parse('<foreignObject><test/></foreignObject>', {
...parserOptions,
ns: Namespaces.SVG
})
const elementForieng = ast.children[0] as ElementNode
const element = elementForieng.children[0] as ElementNode
expect(elementForieng.ns).toBe(Namespaces.SVG)
expect(element.ns).toBe(Namespaces.HTML)
})
})
})

View File

@ -1,14 +1,8 @@
import { ParserOptions, ElementNode, NodeTypes } from '@vue/compiler-core'
import { ParserOptions, NodeTypes, Namespaces } from '@vue/compiler-core'
import { isVoidTag, isHTMLTag, isSVGTag } from '@vue/shared'
import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
import { decodeHtmlBrowser } from './decodeHtmlBrowser'
export const enum DOMNamespaces {
HTML = 0 /* Namespaces.HTML */,
SVG,
MATH_ML
}
export const parserOptions: ParserOptions = {
parseMode: 'html',
isVoidTag,
@ -16,7 +10,7 @@ export const parserOptions: ParserOptions = {
isPreTag: tag => tag === 'pre',
decodeEntities: __BROWSER__ ? decodeHtmlBrowser : undefined,
isBuiltInComponent: (tag: string): symbol | undefined => {
isBuiltInComponent: tag => {
if (tag === 'Transition' || tag === 'transition') {
return TRANSITION
} else if (tag === 'TransitionGroup' || tag === 'transition-group') {
@ -25,12 +19,12 @@ export const parserOptions: ParserOptions = {
},
// https://html.spec.whatwg.org/multipage/parsing.html#tree-construction-dispatcher
getNamespace(tag: string, parent: ElementNode | undefined): DOMNamespaces {
let ns = parent ? parent.ns : DOMNamespaces.HTML
if (parent && ns === DOMNamespaces.MATH_ML) {
getNamespace(tag, parent, rootNamespace) {
let ns = parent ? parent.ns : rootNamespace
if (parent && ns === Namespaces.MATH_ML) {
if (parent.tag === 'annotation-xml') {
if (tag === 'svg') {
return DOMNamespaces.SVG
return Namespaces.SVG
}
if (
parent.props.some(
@ -42,31 +36,31 @@ export const parserOptions: ParserOptions = {
a.value.content === 'application/xhtml+xml')
)
) {
ns = DOMNamespaces.HTML
ns = Namespaces.HTML
}
} else if (
/^m(?:[ions]|text)$/.test(parent.tag) &&
tag !== 'mglyph' &&
tag !== 'malignmark'
) {
ns = DOMNamespaces.HTML
ns = Namespaces.HTML
}
} else if (parent && ns === DOMNamespaces.SVG) {
} else if (parent && ns === Namespaces.SVG) {
if (
parent.tag === 'foreignObject' ||
parent.tag === 'desc' ||
parent.tag === 'title'
) {
ns = DOMNamespaces.HTML
ns = Namespaces.HTML
}
}
if (ns === DOMNamespaces.HTML) {
if (ns === Namespaces.HTML) {
if (tag === 'svg') {
return DOMNamespaces.SVG
return Namespaces.SVG
}
if (tag === 'math') {
return DOMNamespaces.MATH_ML
return Namespaces.MATH_ML
}
}
return ns

View File

@ -15,7 +15,8 @@ import {
PlainElementNode,
JSChildNode,
TextCallNode,
ConstantTypes
ConstantTypes,
Namespaces
} from '@vue/compiler-core'
import {
isVoidTag,
@ -31,7 +32,6 @@ import {
isKnownSvgAttr,
isBooleanAttr
} from '@vue/shared'
import { DOMNamespaces } from '../parserOptions'
export const enum StringifyThresholds {
ELEMENT_WITH_BINDING_COUNT = 5,
@ -148,11 +148,11 @@ const getHoistedNode = (node: TemplateChildNode) =>
node.codegenNode.hoisted
const dataAriaRE = /^(data|aria)-/
const isStringifiableAttr = (name: string, ns: DOMNamespaces) => {
const isStringifiableAttr = (name: string, ns: Namespaces) => {
return (
(ns === DOMNamespaces.HTML
(ns === Namespaces.HTML
? isKnownHtmlAttr(name)
: ns === DOMNamespaces.SVG
: ns === Namespaces.SVG
? isKnownSvgAttr(name)
: false) || dataAriaRE.test(name)
)