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', () => {
|
test('attribute with no value', () => {
|
||||||
const ast = parse('<div id></div>')
|
const ast = parse('<div id></div>')
|
||||||
const element = ast.children[0] as ElementNode
|
const element = ast.children[0] as ElementNode
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { NO } from '@vue/shared'
|
||||||
import {
|
import {
|
||||||
ErrorCodes,
|
ErrorCodes,
|
||||||
createCompilerError,
|
createCompilerError,
|
||||||
@ -30,6 +31,7 @@ import { extend } from '@vue/shared'
|
|||||||
|
|
||||||
export interface ParserOptions {
|
export interface ParserOptions {
|
||||||
isVoidTag?: (tag: string) => boolean // e.g. img, br, hr
|
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
|
getNamespace?: (tag: string, parent: ElementNode | undefined) => Namespace
|
||||||
getTextMode?: (tag: string, ns: Namespace) => TextModes
|
getTextMode?: (tag: string, ns: Namespace) => TextModes
|
||||||
delimiters?: [string, string] // ['{{', '}}']
|
delimiters?: [string, string] // ['{{', '}}']
|
||||||
@ -42,12 +44,19 @@ export interface ParserOptions {
|
|||||||
onError?: (error: CompilerError) => void
|
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: [`{{`, `}}`],
|
delimiters: [`{{`, `}}`],
|
||||||
ignoreSpaces: true,
|
ignoreSpaces: true,
|
||||||
getNamespace: () => Namespaces.HTML,
|
getNamespace: () => Namespaces.HTML,
|
||||||
getTextMode: () => TextModes.DATA,
|
getTextMode: () => TextModes.DATA,
|
||||||
isVoidTag: () => false,
|
isVoidTag: NO,
|
||||||
namedCharacterReferences: {
|
namedCharacterReferences: {
|
||||||
'gt;': '>',
|
'gt;': '>',
|
||||||
'lt;': '<',
|
'lt;': '<',
|
||||||
@ -68,7 +77,7 @@ export const enum TextModes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ParserContext {
|
interface ParserContext {
|
||||||
options: Required<ParserOptions>
|
options: MergedParserOptions
|
||||||
readonly originalSource: string
|
readonly originalSource: string
|
||||||
source: string
|
source: string
|
||||||
offset: number
|
offset: number
|
||||||
@ -428,9 +437,14 @@ function parseTag(
|
|||||||
|
|
||||||
let tagType = ElementTypes.ELEMENT
|
let tagType = ElementTypes.ELEMENT
|
||||||
if (!context.inPre) {
|
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
|
if (tag === 'slot') tagType = ElementTypes.SLOT
|
||||||
else if (tag === 'template') tagType = ElementTypes.TEMPLATE
|
else if (tag === 'template') tagType = ElementTypes.TEMPLATE
|
||||||
else if (/[A-Z-]/.test(tag)) tagType = ElementTypes.COMPONENT
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
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.', () => {
|
test('Strict end tag detection for textarea.', () => {
|
||||||
const ast = parse(
|
const ast = parse(
|
||||||
'<textarea>hello</textarea</textarea0></texTArea a="<>">',
|
'<textarea>hello</textarea</textarea0></texTArea a="<>">',
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
Namespaces,
|
Namespaces,
|
||||||
NodeTypes
|
NodeTypes
|
||||||
} from '@vue/compiler-core'
|
} from '@vue/compiler-core'
|
||||||
|
import { isVoidTag, isHTMLTag, isSVGTag } from '@vue/shared'
|
||||||
|
|
||||||
export const enum DOMNamespaces {
|
export const enum DOMNamespaces {
|
||||||
HTML = Namespaces.HTML,
|
HTML = Namespaces.HTML,
|
||||||
@ -13,6 +14,10 @@ export const enum DOMNamespaces {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const parserOptionsMinimal: ParserOptions = {
|
export const parserOptionsMinimal: ParserOptions = {
|
||||||
|
isVoidTag,
|
||||||
|
|
||||||
|
isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag),
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/parsing.html#tree-construction-dispatcher
|
// https://html.spec.whatwg.org/multipage/parsing.html#tree-construction-dispatcher
|
||||||
getNamespace(tag: string, parent: ElementNode | undefined): DOMNamespaces {
|
getNamespace(tag: string, parent: ElementNode | undefined): DOMNamespaces {
|
||||||
let ns = parent ? parent.ns : DOMNamespaces.HTML
|
let ns = parent ? parent.ns : DOMNamespaces.HTML
|
||||||
@ -75,11 +80,5 @@ export const parserOptionsMinimal: ParserOptions = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return TextModes.DATA
|
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
|
return app
|
||||||
},
|
},
|
||||||
|
|
||||||
component(name: string, component?: Component) {
|
component(name: string, component?: Component): any {
|
||||||
// TODO component name validation
|
|
||||||
if (!component) {
|
if (!component) {
|
||||||
return context.components[name] as any
|
return context.components[name]
|
||||||
} else {
|
} else {
|
||||||
context.components[name] = component
|
context.components[name] = component
|
||||||
return app
|
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 './patchFlags'
|
||||||
|
export * from './element'
|
||||||
export { globalsWhitelist } from './globalsWhitelist'
|
export { globalsWhitelist } from './globalsWhitelist'
|
||||||
|
|
||||||
export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__
|
export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__
|
||||||
@ -8,6 +9,11 @@ export const EMPTY_ARR: [] = []
|
|||||||
|
|
||||||
export const NOOP = () => {}
|
export const NOOP = () => {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Always return false.
|
||||||
|
*/
|
||||||
|
export const NO = () => false
|
||||||
|
|
||||||
export const isOn = (key: string) => key[0] === 'o' && key[1] === 'n'
|
export const isOn = (key: string) => key[0] === 'o' && key[1] === 'n'
|
||||||
|
|
||||||
export const extend = <T extends object, U extends object>(
|
export const extend = <T extends object, U extends object>(
|
||||||
|
Loading…
Reference in New Issue
Block a user