diff --git a/packages/compiler-core/__tests__/parse.spec.ts b/packages/compiler-core/__tests__/parse.spec.ts
index 430ee3151..7b753ff39 100644
--- a/packages/compiler-core/__tests__/parse.spec.ts
+++ b/packages/compiler-core/__tests__/parse.spec.ts
@@ -491,6 +491,27 @@ describe('compiler: parse', () => {
})
})
+ test('self-closing void element', () => {
+ const ast = baseParse('
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('')
const element = ast.children[0]
diff --git a/packages/compiler-core/src/parser/Tokenizer.ts b/packages/compiler-core/src/parser/Tokenizer.ts
index d6fbba877..3fe2656a7 100644
--- a/packages/compiler-core/src/parser/Tokenizer.ts
+++ b/packages/compiler-core/src/parser/Tokenizer.ts
@@ -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()
diff --git a/packages/compiler-core/src/parser/index.ts b/packages/compiler-core/src/parser/index.ts
index aabaaa0e5..8bdcfb38e 100644
--- a/packages/compiler-core/src/parser/index.ts
+++ b/packages/compiler-core/src/parser/index.ts
@@ -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