]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: parse directives
authorEvan You <yyx990803@gmail.com>
Tue, 14 Nov 2023 12:39:08 +0000 (20:39 +0800)
committerEvan You <yyx990803@gmail.com>
Sat, 25 Nov 2023 08:18:29 +0000 (16:18 +0800)
packages/compiler-core/src/parser/index.ts

index 7d00d11d88fe5c2b60564db26b4478e9fd838910..f13226b3efdab55bbd4f2b9969e3d15b799cd7da 100644 (file)
@@ -1,9 +1,12 @@
 import { fromCodePoint } from 'entities/lib/decode.js'
 import {
   AttributeNode,
+  ConstantTypes,
   DirectiveNode,
   ElementNode,
   ElementTypes,
+  ExpressionNode,
+  Namespaces,
   NodeTypes,
   RootNode,
   TemplateChildNode,
@@ -11,28 +14,50 @@ import {
 } from '../ast'
 import { ParserOptions } from '../options'
 import Tokenizer, { CharCodes } from './Tokenizer'
+import { CompilerCompatOptions } from '../compat/compatConfig'
+import { NO, extend, hasOwn } from '@vue/shared'
+import { defaultOnError, defaultOnWarn } from '../errors'
 
-const voidElements = new Set([
-  'area',
-  'base',
-  'basefont',
-  'br',
-  'col',
-  'command',
-  'embed',
-  'frame',
-  'hr',
-  'img',
-  'input',
-  'isindex',
-  'keygen',
-  'link',
-  'meta',
-  'param',
-  'source',
-  'track',
-  'wbr'
-])
+type OptionalOptions =
+  | 'htmlMode'
+  | 'getTextMode' // TODO
+  | 'whitespace'
+  | 'isNativeTag'
+  | 'isBuiltInComponent'
+  | keyof CompilerCompatOptions
+
+type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> &
+  Pick<ParserOptions, OptionalOptions>
+
+// The default decoder only provides escapes for characters reserved as part of
+// the template syntax, and is only used if the custom renderer did not provide
+// a platform-specific decoder.
+const decodeRE = /&(gt|lt|amp|apos|quot);/g
+const decodeMap: Record<string, string> = {
+  gt: '>',
+  lt: '<',
+  amp: '&',
+  apos: "'",
+  quot: '"'
+}
+
+export const defaultParserOptions: MergedParserOptions = {
+  delimiters: [`{{`, `}}`],
+  getNamespace: () => Namespaces.HTML,
+  // getTextMode: () => TextModes.DATA,
+  isVoidTag: NO,
+  isPreTag: NO,
+  isCustomElement: NO,
+  decodeEntities: (rawText: string): string =>
+    rawText.replace(decodeRE, (_, p1) => decodeMap[p1]),
+  onError: defaultOnError,
+  onWarn: defaultOnWarn,
+  comments: __DEV__
+}
+
+const directiveTestRE = /^(v-[A-Za-z0-9-]|:|\.|@|#)/
+const directiveParseRE =
+  /(?:^v-([a-z0-9-]+))?(?:(?::|^\.|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i
 
 const foreignContextElements = new Set(['math', 'svg'])
 
@@ -48,7 +73,7 @@ const htmlIntegrationElements = new Set([
   'title'
 ])
 
-let currentOptions: ParserOptions = {}
+let currentOptions: MergedParserOptions = defaultParserOptions
 let currentRoot: RootNode = createRoot([])
 
 // parser state
@@ -57,8 +82,9 @@ let currentInput = ''
 let currentElement: ElementNode | null = null
 let currentProp: AttributeNode | DirectiveNode | null = null
 let currentAttrValue = ''
+let currentAttrs: Record<string, true> | null = null
 let inPre = 0
-// let inVPre = 0
+let inVPre = 0
 const stack: ElementNode[] = []
 const foreignContext: boolean[] = [false]
 
@@ -79,7 +105,7 @@ const tokenizer = new Tokenizer(
     },
 
     onopentagend(end) {
-      endOpenTag()
+      endOpenTag(end)
     },
 
     onclosetag(start, end) {
@@ -92,7 +118,7 @@ const tokenizer = new Tokenizer(
         foreignContext.shift()
       }
 
-      if (!voidElements.has(name)) {
+      if (!currentOptions.isVoidTag?.(name)) {
         const pos = stack.findIndex(e => e.tag === name)
         if (pos !== -1) {
           for (let index = 0; index <= pos; index++) {
@@ -116,16 +142,114 @@ const tokenizer = new Tokenizer(
     },
 
     onattribname(start, end) {
-      // TODO directives
-      currentProp = {
-        type: NodeTypes.ATTRIBUTE,
-        name: getSlice(start, end),
-        value: undefined,
-        loc: {
-          start: tokenizer.getPositionForIndex(start),
-          // @ts-expect-error to be attached on attribute end
-          end: undefined,
-          source: ''
+      const name = getSlice(start, end)
+      if (hasOwn(currentAttrs!, name)) {
+        // TODO emit error DUPLICATE_ATTRIBUTE
+      } else {
+        currentAttrs![name] = true
+      }
+      if (!inVPre && directiveTestRE.test(name)) {
+        // directive
+        const match = directiveParseRE.exec(name)!
+        const firstChar = name[0]
+        const isPropShorthand = firstChar === '.'
+        const dirName =
+          match[1] ||
+          (isPropShorthand || firstChar === ':'
+            ? 'bind'
+            : firstChar === '@'
+            ? 'on'
+            : 'slot')
+
+        let arg: ExpressionNode | undefined
+        if (match[2]) {
+          const isSlot = dirName === 'slot'
+          // const startOffset = name.lastIndexOf(
+          //   match[2],
+          //   name.length - (match[3]?.length || 0)
+          // )
+          let content = match[2]
+          let isStatic = true
+
+          if (content.startsWith('[')) {
+            isStatic = false
+
+            if (!content.endsWith(']')) {
+              // TODO emitError(
+              //   context,
+              //   ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END
+              // )
+              content = content.slice(1)
+            } else {
+              content = content.slice(1, content.length - 1)
+            }
+          } else if (isSlot) {
+            // #1241 special case for v-slot: vuetify relies extensively on slot
+            // names containing dots. v-slot doesn't have any modifiers and Vue 2.x
+            // supports such usage so we are keeping it consistent with 2.x.
+            content += match[3] || ''
+          }
+
+          arg = {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content,
+            isStatic,
+            constType: isStatic
+              ? ConstantTypes.CAN_STRINGIFY
+              : ConstantTypes.NOT_CONSTANT,
+            // @ts-expect-error TODO
+            loc: {}
+          }
+        }
+
+        const modifiers = match[3] ? match[3].slice(1).split('.') : []
+        if (isPropShorthand) modifiers.push('prop')
+
+        // 2.x compat v-bind:foo.sync -> v-model:foo
+        if (__COMPAT__ && dirName === 'bind' && arg) {
+          // TODO
+          // if (
+          //   modifiers.includes('sync') &&
+          //   checkCompatEnabled(
+          //     CompilerDeprecationTypes.COMPILER_V_BIND_SYNC,
+          //     context,
+          //     loc,
+          //     arg.loc.source
+          //   )
+          // ) {
+          //   dirName = 'model'
+          //   modifiers.splice(modifiers.indexOf('sync'), 1)
+          // }
+          // if (__DEV__ && modifiers.includes('prop')) {
+          //   checkCompatEnabled(
+          //     CompilerDeprecationTypes.COMPILER_V_BIND_PROP,
+          //     context,
+          //     loc
+          //   )
+          // }
+        }
+
+        currentProp = {
+          type: NodeTypes.DIRECTIVE,
+          name: dirName,
+          exp: undefined,
+          arg,
+          modifiers,
+          // @ts-expect-error TODO
+          loc: {}
+        }
+      } else {
+        // plain attribute
+        currentProp = {
+          type: NodeTypes.ATTRIBUTE,
+          name,
+          value: undefined,
+          loc: {
+            start: tokenizer.getPositionForIndex(start),
+            // @ts-expect-error to be attached on attribute end
+            end: undefined,
+            source: ''
+          }
         }
       }
     },
@@ -137,16 +261,28 @@ const tokenizer = new Tokenizer(
     },
     onattribend(_quote, end) {
       if (currentElement) {
-        if (currentProp!.type === NodeTypes.ATTRIBUTE) {
-          // assign value
-          currentProp!.value = {
-            type: NodeTypes.TEXT,
-            content: currentAttrValue,
-            // @ts-expect-error TODO
-            loc: {}
+        if (currentAttrValue) {
+          if (currentProp!.type === NodeTypes.ATTRIBUTE) {
+            // assign value
+            currentProp!.value = {
+              type: NodeTypes.TEXT,
+              content: currentAttrValue,
+              // @ts-expect-error TODO
+              loc: {}
+            }
+          } else {
+            // directive
+            currentProp!.exp = {
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              content: currentAttrValue,
+              isStatic: false,
+              // Treat as non-constant by default. This can be potentially set to
+              // other values by `transformExpression` to make it eligible for hoisting.
+              constType: ConstantTypes.NOT_CONSTANT,
+              // @ts-expect-error TODO
+              loc: {}
+            }
           }
-        } else {
-          // TODO
         }
         currentProp!.loc.end = tokenizer.getPositionForIndex(end)
         currentElement.props.push(currentProp!)
@@ -179,8 +315,7 @@ function emitOpenTag(name: string, start: number) {
   currentElement = {
     type: NodeTypes.ELEMENT,
     tag: name,
-    // TODO refine namespace
-    ns: 0,
+    ns: currentOptions.getNamespace(name, getParent()),
     // TODO refine tag type
     tagType: ElementTypes.ELEMENT,
     props: [],
@@ -193,12 +328,13 @@ function emitOpenTag(name: string, start: number) {
     },
     codegenNode: undefined
   }
+  currentAttrs = {}
 }
 
-function endOpenTag() {
+function endOpenTag(end: number) {
   addNode(currentElement!)
   const name = currentElement!.tag
-  if (!voidElements.has(name)) {
+  if (!currentOptions.isVoidTag(name)) {
     stack.unshift(currentElement!)
     if (htmlMode) {
       if (foreignContextElements.has(name)) {
@@ -207,13 +343,16 @@ function endOpenTag() {
         foreignContext.unshift(false)
       }
     }
+  } else {
+    onCloseTag(currentElement!, end)
   }
   currentElement = null
+  currentAttrs = null
 }
 
 function closeCurrentTag(end: number) {
   const name = currentElement!.tag
-  endOpenTag()
+  endOpenTag(end)
   if (stack[0].tag === name) {
     onCloseTag(stack.shift()!, end)
   }
@@ -321,20 +460,18 @@ function reset() {
   tokenizer.reset()
   currentElement = null
   currentProp = null
+  currentAttrs = null
   currentAttrValue = ''
   stack.length = 0
   foreignContext.length = 1
   foreignContext[0] = false
 }
 
-export function baseParse(
-  input: string,
-  options: ParserOptions = {}
-): RootNode {
+export function baseParse(input: string, options?: ParserOptions): RootNode {
   reset()
   currentInput = input
-  currentOptions = options
-  htmlMode = !!options.htmlMode
+  currentOptions = extend({}, defaultParserOptions, options)
+  htmlMode = !!currentOptions.htmlMode
   const root = (currentRoot = createRoot([]))
   tokenizer.parse(currentInput)
   root.children = condenseWhitespace(root.children)