wip: pass all compiler-sfc tests

This commit is contained in:
Evan You 2023-11-20 17:38:00 +08:00
parent dda4fd526e
commit 059caafc13
5 changed files with 86 additions and 58 deletions

View 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', () => { test('template element with directives', () => {
const ast = baseParse('<template v-if="ok"></template>') const ast = baseParse('<template v-if="ok"></template>')
const element = ast.children[0] const element = ast.children[0]

View File

@ -861,6 +861,9 @@ export default class Tokenizer {
this.buffer = input this.buffer = input
while (this.index < this.buffer.length) { while (this.index < this.buffer.length) {
const c = this.buffer.charCodeAt(this.index) const c = this.buffer.charCodeAt(this.index)
if (c === CharCodes.NewLine) {
this.newlines.push(this.index)
}
switch (this.state) { switch (this.state) {
case State.Text: { case State.Text: {
this.stateText(c) this.stateText(c)
@ -999,9 +1002,6 @@ export default class Tokenizer {
break break
} }
} }
if (c === CharCodes.NewLine) {
this.newlines.push(this.index)
}
this.index++ this.index++
} }
this.cleanup() this.cleanup()

View File

@ -24,7 +24,12 @@ import Tokenizer, {
} from './Tokenizer' } from './Tokenizer'
import { CompilerCompatOptions } from '../compat/compatConfig' import { CompilerCompatOptions } from '../compat/compatConfig'
import { NO, extend } from '@vue/shared' import { NO, extend } from '@vue/shared'
import { defaultOnError, defaultOnWarn } from '../errors' import {
ErrorCodes,
createCompilerError,
defaultOnError,
defaultOnWarn
} from '../errors'
import { forAliasRE, isCoreComponent } from '../utils' import { forAliasRE, isCoreComponent } from '../utils'
import { decodeHTML } from 'entities/lib/decode.js' import { decodeHTML } from 'entities/lib/decode.js'
@ -105,6 +110,11 @@ const tokenizer = new Tokenizer(stack, {
onopentagname(start, end) { onopentagname(start, end) {
const name = getSlice(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 = { currentElement = {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
tag: name, tag: name,
@ -112,7 +122,7 @@ const tokenizer = new Tokenizer(stack, {
tagType: ElementTypes.ELEMENT, // will be refined on tag close tagType: ElementTypes.ELEMENT, // will be refined on tag close
props: [], props: [],
children: [], children: [],
loc: getLoc(start - 1), loc: getLoc(startIndex),
codegenNode: undefined codegenNode: undefined
} }
}, },
@ -137,7 +147,11 @@ const tokenizer = new Tokenizer(stack, {
}, },
onselfclosingtag(end) { onselfclosingtag(end) {
closeCurrentTag(end) const name = currentElement!.tag
endOpenTag(end)
if (stack[0]?.tag === name) {
onCloseTag(stack.shift()!, end)
}
}, },
onattribname(start, end) { onattribname(start, end) {
@ -318,6 +332,13 @@ const tokenizer = new Tokenizer(stack, {
}, },
onend() { onend() {
if (stack.length > 0) {
// has unclosed tag
currentOptions.onError(
// TODO loc info
createCompilerError(ErrorCodes.MISSING_END_TAG_NAME)
)
}
const end = currentInput.length - 1 const end = currentInput.length - 1
for (let index = 0; index < stack.length; index++) { for (let index = 0; index < stack.length; index++) {
onCloseTag(stack[index], end) onCloseTag(stack[index], end)
@ -421,14 +442,6 @@ function endOpenTag(end: number) {
currentElement = null 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) { function onText(content: string, start: number, end: number) {
if (__BROWSER__ && content.includes('&')) { if (__BROWSER__ && content.includes('&')) {
// TODO do not do this in <script> or <style> // 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) { function onCloseTag(el: ElementNode, end: number) {
// attach end position // attach end position
let offset = 0 if (tokenizer.mode === ParseMode.SFC && stack.length === 0) {
while ( // SFC root tag, end position should be inner end
currentInput.charCodeAt(end + offset) !== CharCodes.Gt && if (el.children.length) {
end + offset < currentInput.length el.loc.end = extend({}, el.children[el.children.length - 1].loc.end)
) { } else {
offset++ 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 // refine element type
const { tag, ns } = el 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']) const specialTemplateDir = new Set(['if', 'else', 'else-if', 'for', 'slot'])
function isFragmentTemplate({ tag, props }: ElementNode): boolean { function isFragmentTemplate({ tag, props }: ElementNode): boolean {
if (tag === 'template') { if (tag === 'template') {

View File

@ -1,5 +1,5 @@
import { parse } from '../src' import { parse } from '../src'
import { baseParse, baseCompile } from '@vue/compiler-core' import { baseCompile, createRoot } from '@vue/compiler-core'
import { SourceMapConsumer } from 'source-map-js' import { SourceMapConsumer } from 'source-map-js'
describe('compiler:sfc', () => { describe('compiler:sfc', () => {
@ -122,8 +122,7 @@ h1 { color: red }
line: 3, line: 3,
column: 1, column: 1,
offset: 10 + content.length offset: 10 + content.length
}, }
source: content
}) })
}) })
@ -132,9 +131,8 @@ h1 { color: red }
expect(descriptor.template).toBeTruthy() expect(descriptor.template).toBeTruthy()
expect(descriptor.template!.content).toBeFalsy() expect(descriptor.template!.content).toBeFalsy()
expect(descriptor.template!.loc).toMatchObject({ expect(descriptor.template!.loc).toMatchObject({
start: { line: 1, column: 1, offset: 0 }, start: { line: 1, column: 12, offset: 11 },
end: { line: 1, column: 1, offset: 0 }, end: { line: 1, column: 12, offset: 11 }
source: ''
}) })
}) })
@ -144,8 +142,7 @@ h1 { color: red }
expect(descriptor.template!.content).toBeFalsy() expect(descriptor.template!.content).toBeFalsy()
expect(descriptor.template!.loc).toMatchObject({ expect(descriptor.template!.loc).toMatchObject({
start: { line: 1, column: 11, offset: 10 }, start: { line: 1, column: 11, offset: 10 },
end: { line: 1, column: 11, offset: 10 }, end: { line: 1, column: 11, offset: 10 }
source: ''
}) })
}) })
@ -176,14 +173,12 @@ h1 { color: red }
) )
expect(descriptor.script).toBeTruthy() expect(descriptor.script).toBeTruthy()
expect(descriptor.script!.loc).toMatchObject({ expect(descriptor.script!.loc).toMatchObject({
source: '',
start: { line: 1, column: 9, offset: 8 }, start: { line: 1, column: 9, offset: 8 },
end: { line: 1, column: 9, offset: 8 } end: { line: 1, column: 9, offset: 8 }
}) })
expect(descriptor.scriptSetup).toBeTruthy() expect(descriptor.scriptSetup).toBeTruthy()
expect(descriptor.scriptSetup!.loc).toMatchObject({ expect(descriptor.scriptSetup!.loc).toMatchObject({
source: '\n',
start: { line: 2, column: 15, offset: 32 }, start: { line: 2, column: 15, offset: 32 },
end: { line: 3, column: 1, offset: 33 } end: { line: 3, column: 1, offset: 33 }
}) })
@ -260,11 +255,18 @@ h1 { color: red }
test('custom compiler', () => { test('custom compiler', () => {
const { errors } = parse(`<template><input></template>`, { const { errors } = parse(`<template><input></template>`, {
compiler: { compiler: {
parse: baseParse, parse: (_, options) => {
options.onError!(new Error('foo') as any)
return createRoot([])
},
compile: baseCompile 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', () => { test('treat custom blocks as raw text', () => {

View File

@ -136,7 +136,8 @@ export function parse(
if (node.type !== NodeTypes.ELEMENT) { if (node.type !== NodeTypes.ELEMENT) {
return 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 ( if (
ignoreEmpty && ignoreEmpty &&
node.tag !== 'template' && node.tag !== 'template' &&
@ -284,33 +285,11 @@ function createBlock(
pad: SFCParseOptions['pad'] pad: SFCParseOptions['pad']
): SFCBlock { ): SFCBlock {
const type = node.tag 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 attrs: Record<string, string | true> = {}
const block: SFCBlock = { const block: SFCBlock = {
type, type,
content, content: source.slice(node.loc.start.offset, node.loc.end.offset),
loc, loc: node.loc,
attrs attrs
} }
if (pad) { if (pad) {