diff --git a/packages/compiler-core/__tests__/parse.spec.ts b/packages/compiler-core/__tests__/parse.spec.ts index 0d3fab31..e4dcf2b2 100644 --- a/packages/compiler-core/__tests__/parse.spec.ts +++ b/packages/compiler-core/__tests__/parse.spec.ts @@ -564,6 +564,52 @@ describe('compiler: parse', () => { }) }) + test('native element with `isNativeTag`', () => { + const ast = parse('
', { + 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('
') + + 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('
') const element = ast.children[0] as ElementNode diff --git a/packages/compiler-core/src/parse.ts b/packages/compiler-core/src/parse.ts index 019eddb4..1e8a6d2a 100644 --- a/packages/compiler-core/src/parse.ts +++ b/packages/compiler-core/src/parse.ts @@ -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 = { +// `isNativeTag` is optional, others are required +type MergedParserOptions = Pick< + Required, + Exclude +> & + Pick + +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 + 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 { diff --git a/packages/compiler-dom/__tests__/parse.spec.ts b/packages/compiler-dom/__tests__/parse.spec.ts index fc7c5352..752d08c4 100644 --- a/packages/compiler-dom/__tests__/parse.spec.ts +++ b/packages/compiler-dom/__tests__/parse.spec.ts @@ -154,6 +154,28 @@ describe('DOM parser', () => { }) }) + test('native element', () => { + const ast = parse('
', 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( '', diff --git a/packages/compiler-dom/src/parserOptionsMinimal.ts b/packages/compiler-dom/src/parserOptionsMinimal.ts index fe681ad5..82c7e7ee 100644 --- a/packages/compiler-dom/src/parserOptionsMinimal.ts +++ b/packages/compiler-dom/src/parserOptionsMinimal.ts @@ -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 - ) } } diff --git a/packages/runtime-core/src/apiApp.ts b/packages/runtime-core/src/apiApp.ts index ecf26fab..b80c502c 100644 --- a/packages/runtime-core/src/apiApp.ts +++ b/packages/runtime-core/src/apiApp.ts @@ -110,10 +110,9 @@ export function createAppAPI( 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 diff --git a/packages/shared/src/element.ts b/packages/shared/src/element.ts new file mode 100644 index 00000000..340d2d82 --- /dev/null +++ b/packages/shared/src/element.ts @@ -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) diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 485be989..2b8a1023 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -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 = (