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

View 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()

View 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') {

View 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', () => {

View 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) {