})
})
+ 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]
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)
break
}
}
- if (c === CharCodes.NewLine) {
- this.newlines.push(this.index)
- }
this.index++
}
this.cleanup()
} 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'
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,
tagType: ElementTypes.ELEMENT, // will be refined on tag close
props: [],
children: [],
- loc: getLoc(start - 1),
+ loc: getLoc(startIndex),
codegenNode: undefined
}
},
},
onselfclosingtag(end) {
- closeCurrentTag(end)
+ const name = currentElement!.tag
+ endOpenTag(end)
+ if (stack[0]?.tag === name) {
+ onCloseTag(stack.shift()!, end)
+ }
},
onattribname(start, end) {
},
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)
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>
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
}
}
+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') {
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', () => {
line: 3,
column: 1,
offset: 10 + content.length
- },
- source: content
+ }
})
})
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 }
})
})
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 }
})
})
)
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 }
})
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', () => {
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' &&
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) {