mirror of https://github.com/vuejs/core.git
feat(compiler-core): support specifying root namespace when parsing
This commit is contained in:
parent
a1b10a21ac
commit
40f72d5e50
|
@ -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 {
|
||||
|
|
|
@ -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 ['{{', '}}']
|
||||
*/
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue