]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: pass all compiler-sfc tests
authorEvan You <yyx990803@gmail.com>
Mon, 20 Nov 2023 09:38:00 +0000 (17:38 +0800)
committerEvan You <yyx990803@gmail.com>
Sat, 25 Nov 2023 08:18:29 +0000 (16:18 +0800)
packages/compiler-core/__tests__/parse.spec.ts
packages/compiler-core/src/parser/Tokenizer.ts
packages/compiler-core/src/parser/index.ts
packages/compiler-sfc/__tests__/parse.spec.ts
packages/compiler-sfc/src/parse.ts

index 430ee3151d0e7937f7d59fd2d80c525ce4661374..7b753ff39b68e59f6e5c09d8a9d26bc54e0986e9 100644 (file)
@@ -491,6 +491,27 @@ describe('compiler: parse', () => {
       })
     })
 
+    test('self-closing void element', () => {
+      const ast = baseParse('<img/>after', {
+        isVoidTag: tag => tag === 'img'
+      })
+      const element = ast.children[0] as ElementNode
+
+      expect(element).toStrictEqual({
+        type: NodeTypes.ELEMENT,
+        ns: Namespaces.HTML,
+        tag: 'img',
+        tagType: ElementTypes.ELEMENT,
+        codegenNode: undefined,
+        props: [],
+        children: [],
+        loc: {
+          start: { offset: 0, line: 1, column: 1 },
+          end: { offset: 6, line: 1, column: 7 }
+        }
+      })
+    })
+
     test('template element with directives', () => {
       const ast = baseParse('<template v-if="ok"></template>')
       const element = ast.children[0]
index d6fbba877680cb7f8e1532481056eda0b9bcdb9f..3fe2656a759638dc0d573341e0721098cda39d43 100644 (file)
@@ -861,6 +861,9 @@ export default class Tokenizer {
     this.buffer = input
     while (this.index < this.buffer.length) {
       const c = this.buffer.charCodeAt(this.index)
+      if (c === CharCodes.NewLine) {
+        this.newlines.push(this.index)
+      }
       switch (this.state) {
         case State.Text: {
           this.stateText(c)
@@ -999,9 +1002,6 @@ export default class Tokenizer {
           break
         }
       }
-      if (c === CharCodes.NewLine) {
-        this.newlines.push(this.index)
-      }
       this.index++
     }
     this.cleanup()
index aabaaa0e51373ad0f50ecd4ae0c47b8b0ccf68b1..8bdcfb38e3fe488e5008abf1758ff430bbb5960c 100644 (file)
@@ -24,7 +24,12 @@ import Tokenizer, {
 } from './Tokenizer'
 import { CompilerCompatOptions } from '../compat/compatConfig'
 import { NO, extend } from '@vue/shared'
-import { defaultOnError, defaultOnWarn } from '../errors'
+import {
+  ErrorCodes,
+  createCompilerError,
+  defaultOnError,
+  defaultOnWarn
+} from '../errors'
 import { forAliasRE, isCoreComponent } from '../utils'
 import { decodeHTML } from 'entities/lib/decode.js'
 
@@ -105,6 +110,11 @@ const tokenizer = new Tokenizer(stack, {
 
   onopentagname(start, end) {
     const name = getSlice(start, end)
+    // in SFC mode, root-level tags locations are for its inner content.
+    const startIndex =
+      tokenizer.mode === ParseMode.SFC && stack.length === 0
+        ? end + fastForward(end, CharCodes.Gt) + 1
+        : start - 1
     currentElement = {
       type: NodeTypes.ELEMENT,
       tag: name,
@@ -112,7 +122,7 @@ const tokenizer = new Tokenizer(stack, {
       tagType: ElementTypes.ELEMENT, // will be refined on tag close
       props: [],
       children: [],
-      loc: getLoc(start - 1),
+      loc: getLoc(startIndex),
       codegenNode: undefined
     }
   },
@@ -137,7 +147,11 @@ const tokenizer = new Tokenizer(stack, {
   },
 
   onselfclosingtag(end) {
-    closeCurrentTag(end)
+    const name = currentElement!.tag
+    endOpenTag(end)
+    if (stack[0]?.tag === name) {
+      onCloseTag(stack.shift()!, end)
+    }
   },
 
   onattribname(start, end) {
@@ -318,6 +332,13 @@ const tokenizer = new Tokenizer(stack, {
   },
 
   onend() {
+    if (stack.length > 0) {
+      // has unclosed tag
+      currentOptions.onError(
+        // TODO loc info
+        createCompilerError(ErrorCodes.MISSING_END_TAG_NAME)
+      )
+    }
     const end = currentInput.length - 1
     for (let index = 0; index < stack.length; index++) {
       onCloseTag(stack[index], end)
@@ -421,14 +442,6 @@ function endOpenTag(end: number) {
   currentElement = null
 }
 
-function closeCurrentTag(end: number) {
-  const name = currentElement!.tag
-  endOpenTag(end)
-  if (stack[0].tag === name) {
-    onCloseTag(stack.shift()!, end)
-  }
-}
-
 function onText(content: string, start: number, end: number) {
   if (__BROWSER__ && content.includes('&')) {
     // TODO do not do this in <script> or <style>
@@ -451,14 +464,16 @@ function onText(content: string, start: number, end: number) {
 
 function onCloseTag(el: ElementNode, end: number) {
   // attach end position
-  let offset = 0
-  while (
-    currentInput.charCodeAt(end + offset) !== CharCodes.Gt &&
-    end + offset < currentInput.length
-  ) {
-    offset++
+  if (tokenizer.mode === ParseMode.SFC && stack.length === 0) {
+    // SFC root tag, end position should be inner end
+    if (el.children.length) {
+      el.loc.end = extend({}, el.children[el.children.length - 1].loc.end)
+    } else {
+      el.loc.end = extend({}, el.loc.start)
+    }
+  } else {
+    el.loc.end = tokenizer.getPos(end + fastForward(end, CharCodes.Gt) + 1)
   }
-  el.loc.end = tokenizer.getPos(end + offset + 1)
 
   // refine element type
   const { tag, ns } = el
@@ -491,6 +506,17 @@ function onCloseTag(el: ElementNode, end: number) {
   }
 }
 
+function fastForward(start: number, c: number) {
+  let offset = 0
+  while (
+    currentInput.charCodeAt(start + offset) !== CharCodes.Gt &&
+    start + offset < currentInput.length
+  ) {
+    offset++
+  }
+  return offset
+}
+
 const specialTemplateDir = new Set(['if', 'else', 'else-if', 'for', 'slot'])
 function isFragmentTemplate({ tag, props }: ElementNode): boolean {
   if (tag === 'template') {
index c7a17ab1739957d4c4b8c1fcff1774071d7942b4..65a318c0bb07f98894ab33ac591488ba8a2ef0c6 100644 (file)
@@ -1,5 +1,5 @@
 import { parse } from '../src'
-import { baseParse, baseCompile } from '@vue/compiler-core'
+import { baseCompile, createRoot } from '@vue/compiler-core'
 import { SourceMapConsumer } from 'source-map-js'
 
 describe('compiler:sfc', () => {
@@ -122,8 +122,7 @@ h1 { color: red }
         line: 3,
         column: 1,
         offset: 10 + content.length
-      },
-      source: content
+      }
     })
   })
 
@@ -132,9 +131,8 @@ h1 { color: red }
     expect(descriptor.template).toBeTruthy()
     expect(descriptor.template!.content).toBeFalsy()
     expect(descriptor.template!.loc).toMatchObject({
-      start: { line: 1, column: 1, offset: 0 },
-      end: { line: 1, column: 1, offset: 0 },
-      source: ''
+      start: { line: 1, column: 12, offset: 11 },
+      end: { line: 1, column: 12, offset: 11 }
     })
   })
 
@@ -144,8 +142,7 @@ h1 { color: red }
     expect(descriptor.template!.content).toBeFalsy()
     expect(descriptor.template!.loc).toMatchObject({
       start: { line: 1, column: 11, offset: 10 },
-      end: { line: 1, column: 11, offset: 10 },
-      source: ''
+      end: { line: 1, column: 11, offset: 10 }
     })
   })
 
@@ -176,14 +173,12 @@ h1 { color: red }
     )
     expect(descriptor.script).toBeTruthy()
     expect(descriptor.script!.loc).toMatchObject({
-      source: '',
       start: { line: 1, column: 9, offset: 8 },
       end: { line: 1, column: 9, offset: 8 }
     })
 
     expect(descriptor.scriptSetup).toBeTruthy()
     expect(descriptor.scriptSetup!.loc).toMatchObject({
-      source: '\n',
       start: { line: 2, column: 15, offset: 32 },
       end: { line: 3, column: 1, offset: 33 }
     })
@@ -260,11 +255,18 @@ h1 { color: red }
   test('custom compiler', () => {
     const { errors } = parse(`<template><input></template>`, {
       compiler: {
-        parse: baseParse,
+        parse: (_, options) => {
+          options.onError!(new Error('foo') as any)
+          return createRoot([])
+        },
         compile: baseCompile
       }
     })
-    expect(errors.length).toBe(1)
+    expect(errors.length).toBe(2)
+    // error thrown by the custom parse
+    expect(errors[0].message).toBe('foo')
+    // error thrown based on the returned root
+    expect(errors[1].message).toMatch('At least one')
   })
 
   test('treat custom blocks as raw text', () => {
index 3612d5a393e9b8316597de7309197f1c9d3bc1eb..31087394a370640b81b21868e3f85efa6c90386a 100644 (file)
@@ -136,7 +136,8 @@ export function parse(
     if (node.type !== NodeTypes.ELEMENT) {
       return
     }
-    // we only want to keep the nodes that are not empty (when the tag is not a template)
+    // we only want to keep the nodes that are not empty
+    // (when the tag is not a template)
     if (
       ignoreEmpty &&
       node.tag !== 'template' &&
@@ -284,33 +285,11 @@ function createBlock(
   pad: SFCParseOptions['pad']
 ): SFCBlock {
   const type = node.tag
-  let { start, end } = node.loc
-  let content = ''
-  if (node.children.length) {
-    start = node.children[0].loc.start
-    end = node.children[node.children.length - 1].loc.end
-    content = source.slice(start.offset, end.offset)
-  } else {
-    const offset = source.indexOf(`</`, start.offset)
-    if (offset > -1) {
-      start = {
-        line: start.line,
-        column: start.column + offset,
-        offset: start.offset + offset
-      }
-    }
-    end = { ...start }
-  }
-  const loc = {
-    source: content,
-    start,
-    end
-  }
   const attrs: Record<string, string | true> = {}
   const block: SFCBlock = {
     type,
-    content,
-    loc,
+    content: source.slice(node.loc.start.offset, node.loc.end.offset),
+    loc: node.loc,
     attrs
   }
   if (pad) {