]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler): add isNativeTag option for determining element type (#139)
author月迷津渡 <CodeDaraW@gmail.com>
Thu, 10 Oct 2019 18:54:06 +0000 (02:54 +0800)
committerEvan You <yyx990803@gmail.com>
Thu, 10 Oct 2019 18:54:06 +0000 (14:54 -0400)
packages/compiler-core/__tests__/parse.spec.ts
packages/compiler-core/src/parse.ts
packages/compiler-dom/__tests__/parse.spec.ts
packages/compiler-dom/src/parserOptionsMinimal.ts
packages/runtime-core/src/apiApp.ts
packages/shared/src/element.ts [new file with mode: 0644]
packages/shared/src/index.ts

index 0d3fab316182f419394c448baa6f89fed12e5a28..e4dcf2b2b549b23494979fd12c53f03492ab05b2 100644 (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
index 019eddb49829d8837f949c882cb5c6639a2e114b..1e8a6d2a81accae13bd345fbd31212e08c299619 100644 (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 {
index fc7c53529e10adae050ba5e5d4cd74ba9c8e260d..752d08c4ff54cdbd6b075fc8b0860ee3e075c575 100644 (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="<>">',
index fe681ad51d7a66450beba4b674e7460bd99994d8..82c7e7ee1573a7ef85a4a228bc26d840198873b3 100644 (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
-    )
   }
 }
index ecf26fabc4ef95aea5a8264cac45ae7654afb198..b80c502cf524e7a2e133965ac2f386338a4625fd 100644 (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
diff --git a/packages/shared/src/element.ts b/packages/shared/src/element.ts
new file mode 100644 (file)
index 0000000..340d2d8
--- /dev/null
@@ -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)
index 485be989cd8a4bca1a457ce38cc47153a49f387c..2b8a1023f4f12faae54c3c490d299c351775fab1 100644 (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>(