feat(compiler): add isNativeTag option for determining element type (#139)

This commit is contained in:
月迷津渡 2019-10-11 02:54:06 +08:00 committed by Evan You
parent 46d875f4e8
commit 78f60347dc
7 changed files with 275 additions and 13 deletions

View File

@ -564,6 +564,52 @@ describe('compiler: parse', () => {
})
})
test('native element with `isNativeTag`', () => {
const ast = parse('<div></div><comp></comp><Comp></Comp>', {
isNativeTag: tag => tag === 'div'
})
expect(ast.children[0]).toMatchObject({
type: NodeTypes.ELEMENT,
tag: 'div',
tagType: ElementTypes.ELEMENT
})
expect(ast.children[1]).toMatchObject({
type: NodeTypes.ELEMENT,
tag: 'comp',
tagType: ElementTypes.COMPONENT
})
expect(ast.children[2]).toMatchObject({
type: NodeTypes.ELEMENT,
tag: 'Comp',
tagType: ElementTypes.COMPONENT
})
})
test('native element without `isNativeTag`', () => {
const ast = parse('<div></div><comp></comp><Comp></Comp>')
expect(ast.children[0]).toMatchObject({
type: NodeTypes.ELEMENT,
tag: 'div',
tagType: ElementTypes.ELEMENT
})
expect(ast.children[1]).toMatchObject({
type: NodeTypes.ELEMENT,
tag: 'comp',
tagType: ElementTypes.ELEMENT
})
expect(ast.children[2]).toMatchObject({
type: NodeTypes.ELEMENT,
tag: 'Comp',
tagType: ElementTypes.COMPONENT
})
})
test('attribute with no value', () => {
const ast = parse('<div id></div>')
const element = ast.children[0] as ElementNode

View File

@ -1,3 +1,4 @@
import { NO } from '@vue/shared'
import {
ErrorCodes,
createCompilerError,
@ -30,6 +31,7 @@ import { extend } from '@vue/shared'
export interface ParserOptions {
isVoidTag?: (tag: string) => boolean // e.g. img, br, hr
isNativeTag?: (tag: string) => boolean // e.g. loading-indicator in weex
getNamespace?: (tag: string, parent: ElementNode | undefined) => Namespace
getTextMode?: (tag: string, ns: Namespace) => TextModes
delimiters?: [string, string] // ['{{', '}}']
@ -42,12 +44,19 @@ export interface ParserOptions {
onError?: (error: CompilerError) => void
}
export const defaultParserOptions: Required<ParserOptions> = {
// `isNativeTag` is optional, others are required
type MergedParserOptions = Pick<
Required<ParserOptions>,
Exclude<keyof ParserOptions, 'isNativeTag'>
> &
Pick<ParserOptions, 'isNativeTag'>
export const defaultParserOptions: MergedParserOptions = {
delimiters: [`{{`, `}}`],
ignoreSpaces: true,
getNamespace: () => Namespaces.HTML,
getTextMode: () => TextModes.DATA,
isVoidTag: () => false,
isVoidTag: NO,
namedCharacterReferences: {
'gt;': '>',
'lt;': '<',
@ -68,7 +77,7 @@ export const enum TextModes {
}
interface ParserContext {
options: Required<ParserOptions>
options: MergedParserOptions
readonly originalSource: string
source: string
offset: number
@ -428,9 +437,14 @@ function parseTag(
let tagType = ElementTypes.ELEMENT
if (!context.inPre) {
if (context.options.isNativeTag) {
if (!context.options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT
} else {
if (/^[A-Z]/.test(tag)) tagType = ElementTypes.COMPONENT
}
if (tag === 'slot') tagType = ElementTypes.SLOT
else if (tag === 'template') tagType = ElementTypes.TEMPLATE
else if (/[A-Z-]/.test(tag)) tagType = ElementTypes.COMPONENT
}
return {

View File

@ -154,6 +154,28 @@ describe('DOM parser', () => {
})
})
test('native element', () => {
const ast = parse('<div></div><comp></comp><Comp></Comp>', parserOptions)
expect(ast.children[0]).toMatchObject({
type: NodeTypes.ELEMENT,
tag: 'div',
tagType: ElementTypes.ELEMENT
})
expect(ast.children[1]).toMatchObject({
type: NodeTypes.ELEMENT,
tag: 'comp',
tagType: ElementTypes.COMPONENT
})
expect(ast.children[2]).toMatchObject({
type: NodeTypes.ELEMENT,
tag: 'Comp',
tagType: ElementTypes.COMPONENT
})
})
test('Strict end tag detection for textarea.', () => {
const ast = parse(
'<textarea>hello</textarea</textarea0></texTArea a="<>">',

View File

@ -5,6 +5,7 @@ import {
Namespaces,
NodeTypes
} from '@vue/compiler-core'
import { isVoidTag, isHTMLTag, isSVGTag } from '@vue/shared'
export const enum DOMNamespaces {
HTML = Namespaces.HTML,
@ -13,6 +14,10 @@ export const enum DOMNamespaces {
}
export const parserOptionsMinimal: ParserOptions = {
isVoidTag,
isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag),
// 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
@ -75,11 +80,5 @@ export const parserOptionsMinimal: ParserOptions = {
}
}
return TextModes.DATA
},
isVoidTag(tag: string): boolean {
return /^(?:area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/i.test(
tag
)
}
}

View File

@ -110,10 +110,9 @@ export function createAppAPI<HostNode, HostElement>(
return app
},
component(name: string, component?: Component) {
// TODO component name validation
component(name: string, component?: Component): any {
if (!component) {
return context.components[name] as any
return context.components[name]
} else {
context.components[name] = component
return app

View File

@ -0,0 +1,176 @@
const HTMLTagSet = new Set([
'html',
'body',
'base',
'head',
'link',
'meta',
'style',
'title',
'address',
'article',
'aside',
'footer',
'header',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'hgroup',
'nav',
'section',
'div',
'dd',
'dl',
'dt',
'figcaption',
'figure',
'picture',
'hr',
'img',
'li',
'main',
'ol',
'p',
'pre',
'ul',
'a',
'b',
'abbr',
'bdi',
'bdo',
'br',
'cite',
'code',
'data',
'dfn',
'em',
'i',
'kbd',
'mark',
'q',
'rp',
'rt',
'rtc',
'ruby',
's',
'samp',
'small',
'span',
'strong',
'sub',
'sup',
'time',
'u',
'var',
'wbr',
'area',
'audio',
'map',
'track',
'video',
'embed',
'object',
'param',
'source',
'canvas',
'script',
'noscript',
'del',
'ins',
'caption',
'col',
'colgroup',
'table',
'thead',
'tbody',
'td',
'th',
'tr',
'button',
'datalist',
'fieldset',
'form',
'input',
'label',
'legend',
'meter',
'optgroup',
'option',
'output',
'progress',
'select',
'textarea',
'details',
'dialog',
'menu',
'menuitem',
'summary',
'content',
'element',
'shadow',
'template',
'blockquote',
'iframe',
'tfoot'
])
/**
* this list is intentionally selective, only covering SVG elements that may
* contain child elements.
*/
const SVGTagSet = new Set([
'svg',
'animate',
'circle',
'clippath',
'cursor',
'defs',
'desc',
'ellipse',
'filter',
'font-face',
'foreignObject',
'g',
'glyph',
'image',
'line',
'marker',
'mask',
'missing-glyph',
'path',
'pattern',
'polygon',
'polyline',
'rect',
'switch',
'symbol',
'text',
'textpath',
'tspan',
'use',
'view'
])
const VoidTagSet = new Set([
'area',
'base',
'br',
'col',
'embed',
'hr',
'img',
'input',
'link',
'meta',
'param',
'source',
'track',
'wbr'
])
export const isVoidTag = (tag: string) => VoidTagSet.has(tag)
export const isHTMLTag = (tag: string) => HTMLTagSet.has(tag)
export const isSVGTag = (tag: string) => SVGTagSet.has(tag)

View File

@ -1,4 +1,5 @@
export * from './patchFlags'
export * from './element'
export { globalsWhitelist } from './globalsWhitelist'
export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__
@ -8,6 +9,11 @@ export const EMPTY_ARR: [] = []
export const NOOP = () => {}
/**
* Always return false.
*/
export const NO = () => false
export const isOn = (key: string) => key[0] === 'o' && key[1] === 'n'
export const extend = <T extends object, U extends object>(