Skip to content

Commit

Permalink
feat(compiler-core): support specifying root namespace when parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Nov 25, 2023
1 parent a1b10a2 commit 40f72d5
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 56 deletions.
7 changes: 4 additions & 3 deletions packages/compiler-core/src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
31 changes: 29 additions & 2 deletions packages/compiler-core/src/options.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
*/
Expand All @@ -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 ['{{', '}}']
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/compiler-core/src/parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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: [],
Expand Down
65 changes: 39 additions & 26 deletions packages/compiler-dom/__tests__/parse.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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: [],
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -433,26 +434,38 @@ 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', () => {
const ast = parse('<html><svg></svg></html>', parserOptions)
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', () => {
const ast = parse('<html><math></math></html>', parserOptions)
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)
})
})
})
32 changes: 13 additions & 19 deletions packages/compiler-dom/src/parserOptions.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
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,
isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag),
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') {
Expand All @@ -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(
Expand All @@ -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
Expand Down
10 changes: 5 additions & 5 deletions packages/compiler-dom/src/transforms/stringifyStatic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
PlainElementNode,
JSChildNode,
TextCallNode,
ConstantTypes
ConstantTypes,
Namespaces
} from '@vue/compiler-core'
import {
isVoidTag,
Expand All @@ -31,7 +32,6 @@ import {
isKnownSvgAttr,
isBooleanAttr
} from '@vue/shared'
import { DOMNamespaces } from '../parserOptions'

export const enum StringifyThresholds {
ELEMENT_WITH_BINDING_COUNT = 5,
Expand Down Expand Up @@ -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)
)
Expand Down

0 comments on commit 40f72d5

Please sign in to comment.