feat(compiler): add isNativeTag option for determining element type (#139)
This commit is contained in:
parent
46d875f4e8
commit
78f60347dc
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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="<>">',
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
176
packages/shared/src/element.ts
Normal file
176
packages/shared/src/element.ts
Normal 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)
|
@ -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>(
|
||||
|
Loading…
Reference in New Issue
Block a user