import { ParserOptions } from '../src/options'
-import { parse, TextModes } from '../src/parse'
+import { baseParse, TextModes } from '../src/parse'
import { ErrorCodes } from '../src/errors'
import {
CommentNode,
describe('compiler: parse', () => {
describe('Text', () => {
test('simple text', () => {
- const ast = parse('some text')
+ const ast = baseParse('some text')
const text = ast.children[0] as TextNode
expect(text).toStrictEqual({
})
test('simple text with invalid end tag', () => {
- const ast = parse('some text</div>', {
+ const ast = baseParse('some text</div>', {
onError: () => {}
})
const text = ast.children[0] as TextNode
})
test('text with interpolation', () => {
- const ast = parse('some {{ foo + bar }} text')
+ const ast = baseParse('some {{ foo + bar }} text')
const text1 = ast.children[0] as TextNode
const text2 = ast.children[2] as TextNode
})
test('text with interpolation which has `<`', () => {
- const ast = parse('some {{ a<b && c>d }} text')
+ const ast = baseParse('some {{ a<b && c>d }} text')
const text1 = ast.children[0] as TextNode
const text2 = ast.children[2] as TextNode
})
test('text with mix of tags and interpolations', () => {
- const ast = parse('some <span>{{ foo < bar + foo }} text</span>')
+ const ast = baseParse('some <span>{{ foo < bar + foo }} text</span>')
const text1 = ast.children[0] as TextNode
const text2 = (ast.children[1] as ElementNode).children![1] as TextNode
})
test('lonly "<" don\'t separate nodes', () => {
- const ast = parse('a < b', {
+ const ast = baseParse('a < b', {
onError: err => {
if (err.code !== ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME) {
throw err
})
test('lonly "{{" don\'t separate nodes', () => {
- const ast = parse('a {{ b', {
+ const ast = baseParse('a {{ b', {
onError: error => {
if (error.code !== ErrorCodes.X_MISSING_INTERPOLATION_END) {
throw error
test('HTML entities compatibility in text (https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state).', () => {
const spy = jest.fn()
- const ast = parse('&ersand;', {
+ const ast = baseParse('&ersand;', {
namedCharacterReferences: { amp: '&' },
onError: spy
})
test('HTML entities compatibility in attribute (https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state).', () => {
const spy = jest.fn()
- const ast = parse(
+ const ast = baseParse(
'<div a="&ersand;" b="&ersand;" c="&!"></div>',
{
namedCharacterReferences: { amp: '&', 'amp;': '&' },
test('Some control character reference should be replaced.', () => {
const spy = jest.fn()
- const ast = parse('†', { onError: spy })
+ const ast = baseParse('†', { onError: spy })
const text = ast.children[0] as TextNode
expect(text).toStrictEqual({
describe('Interpolation', () => {
test('simple interpolation', () => {
- const ast = parse('{{message}}')
+ const ast = baseParse('{{message}}')
const interpolation = ast.children[0] as InterpolationNode
expect(interpolation).toStrictEqual({
})
test('it can have tag-like notation', () => {
- const ast = parse('{{ a<b }}')
+ const ast = baseParse('{{ a<b }}')
const interpolation = ast.children[0] as InterpolationNode
expect(interpolation).toStrictEqual({
})
test('it can have tag-like notation (2)', () => {
- const ast = parse('{{ a<b }}{{ c>d }}')
+ const ast = baseParse('{{ a<b }}{{ c>d }}')
const interpolation1 = ast.children[0] as InterpolationNode
const interpolation2 = ast.children[1] as InterpolationNode
})
test('it can have tag-like notation (3)', () => {
- const ast = parse('<div>{{ "</div>" }}</div>')
+ const ast = baseParse('<div>{{ "</div>" }}</div>')
const element = ast.children[0] as ElementNode
const interpolation = element.children[0] as InterpolationNode
})
test('custom delimiters', () => {
- const ast = parse('<p>{msg}</p>', {
+ const ast = baseParse('<p>{msg}</p>', {
delimiters: ['{', '}']
})
const element = ast.children[0] as ElementNode
describe('Comment', () => {
test('empty comment', () => {
- const ast = parse('<!---->')
+ const ast = baseParse('<!---->')
const comment = ast.children[0] as CommentNode
expect(comment).toStrictEqual({
})
test('simple comment', () => {
- const ast = parse('<!--abc-->')
+ const ast = baseParse('<!--abc-->')
const comment = ast.children[0] as CommentNode
expect(comment).toStrictEqual({
})
test('two comments', () => {
- const ast = parse('<!--abc--><!--def-->')
+ const ast = baseParse('<!--abc--><!--def-->')
const comment1 = ast.children[0] as CommentNode
const comment2 = ast.children[1] as CommentNode
describe('Element', () => {
test('simple div', () => {
- const ast = parse('<div>hello</div>')
+ const ast = baseParse('<div>hello</div>')
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
})
test('empty', () => {
- const ast = parse('<div></div>')
+ const ast = baseParse('<div></div>')
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
})
test('self closing', () => {
- const ast = parse('<div/>after')
+ const ast = baseParse('<div/>after')
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
})
test('void element', () => {
- const ast = parse('<img>after', {
+ const ast = baseParse('<img>after', {
isVoidTag: tag => tag === 'img'
})
const element = ast.children[0] as ElementNode
})
test('native element with `isNativeTag`', () => {
- const ast = parse('<div></div><comp></comp><Comp></Comp>', {
+ const ast = baseParse('<div></div><comp></comp><Comp></Comp>', {
isNativeTag: tag => tag === 'div'
})
})
test('native element without `isNativeTag`', () => {
- const ast = parse('<div></div><comp></comp><Comp></Comp>')
+ const ast = baseParse('<div></div><comp></comp><Comp></Comp>')
expect(ast.children[0]).toMatchObject({
type: NodeTypes.ELEMENT,
})
test('custom element', () => {
- const ast = parse('<div></div><comp></comp>', {
+ const ast = baseParse('<div></div><comp></comp>', {
isNativeTag: tag => tag === 'div',
isCustomElement: tag => tag === 'comp'
})
})
test('attribute with no value', () => {
- const ast = parse('<div id></div>')
+ const ast = baseParse('<div id></div>')
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
})
test('attribute with empty value, double quote', () => {
- const ast = parse('<div id=""></div>')
+ const ast = baseParse('<div id=""></div>')
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
})
test('attribute with empty value, single quote', () => {
- const ast = parse("<div id=''></div>")
+ const ast = baseParse("<div id=''></div>")
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
})
test('attribute with value, double quote', () => {
- const ast = parse('<div id=">\'"></div>')
+ const ast = baseParse('<div id=">\'"></div>')
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
})
test('attribute with value, single quote', () => {
- const ast = parse("<div id='>\"'></div>")
+ const ast = baseParse("<div id='>\"'></div>")
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
})
test('attribute with value, unquoted', () => {
- const ast = parse('<div id=a/></div>')
+ const ast = baseParse('<div id=a/></div>')
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
})
test('multiple attributes', () => {
- const ast = parse('<div id=a class="c" inert style=\'\'></div>')
+ const ast = baseParse('<div id=a class="c" inert style=\'\'></div>')
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
})
test('directive with no value', () => {
- const ast = parse('<div v-if/>')
+ const ast = baseParse('<div v-if/>')
const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({
})
test('directive with value', () => {
- const ast = parse('<div v-if="a"/>')
+ const ast = baseParse('<div v-if="a"/>')
const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({
})
test('directive with argument', () => {
- const ast = parse('<div v-on:click/>')
+ const ast = baseParse('<div v-on:click/>')
const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({
})
test('directive with a modifier', () => {
- const ast = parse('<div v-on.enter/>')
+ const ast = baseParse('<div v-on.enter/>')
const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({
})
test('directive with two modifiers', () => {
- const ast = parse('<div v-on.enter.exact/>')
+ const ast = baseParse('<div v-on.enter.exact/>')
const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({
})
test('directive with argument and modifiers', () => {
- const ast = parse('<div v-on:click.enter.exact/>')
+ const ast = baseParse('<div v-on:click.enter.exact/>')
const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({
})
test('v-bind shorthand', () => {
- const ast = parse('<div :a=b />')
+ const ast = baseParse('<div :a=b />')
const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({
})
test('v-bind shorthand with modifier', () => {
- const ast = parse('<div :a.sync=b />')
+ const ast = baseParse('<div :a.sync=b />')
const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({
})
test('v-on shorthand', () => {
- const ast = parse('<div @a=b />')
+ const ast = baseParse('<div @a=b />')
const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({
})
test('v-on shorthand with modifier', () => {
- const ast = parse('<div @a.enter=b />')
+ const ast = baseParse('<div @a.enter=b />')
const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({
})
test('v-slot shorthand', () => {
- const ast = parse('<Comp #a="{ b }" />')
+ const ast = baseParse('<Comp #a="{ b }" />')
const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({
})
test('v-pre', () => {
- const ast = parse(
+ const ast = baseParse(
`<div v-pre :id="foo"><Comp/>{{ bar }}</div>\n` +
`<div :id="foo"><Comp/>{{ bar }}</div>`
)
})
test('end tags are case-insensitive.', () => {
- const ast = parse('<div>hello</DIV>after')
+ const ast = baseParse('<div>hello</DIV>after')
const element = ast.children[0] as ElementNode
const text = element.children[0] as TextNode
})
test('self closing single tag', () => {
- const ast = parse('<div :class="{ some: condition }" />')
+ const ast = baseParse('<div :class="{ some: condition }" />')
expect(ast.children).toHaveLength(1)
expect(ast.children[0]).toMatchObject({ tag: 'div' })
})
test('self closing multiple tag', () => {
- const ast = parse(
+ const ast = baseParse(
`<div :class="{ some: condition }" />\n` +
`<p v-bind:style="{ color: 'red' }"/>`
)
})
test('valid html', () => {
- const ast = parse(
+ const ast = baseParse(
`<div :class="{ some: condition }">\n` +
` <p v-bind:style="{ color: 'red' }"/>\n` +
` <!-- a comment with <html> inside it -->\n` +
test('invalid html', () => {
expect(() => {
- parse(`<div>\n<span>\n</div>\n</span>`)
+ baseParse(`<div>\n<span>\n</div>\n</span>`)
}).toThrow('Element is missing end tag.')
const spy = jest.fn()
- const ast = parse(`<div>\n<span>\n</div>\n</span>`, {
+ const ast = baseParse(`<div>\n<span>\n</div>\n</span>`, {
onError: spy
})
})
test('parse with correct location info', () => {
- const [foo, bar, but, baz] = parse(
+ const [foo, bar, but, baz] = baseParse(
`
foo
is {{ bar }} but {{ baz }}`.trim()
describe('namedCharacterReferences option', () => {
test('use the given map', () => {
- const ast: any = parse('&∪︀', {
+ const ast: any = baseParse('&∪︀', {
namedCharacterReferences: {
'cups;': '\u222A\uFE00' // UNION with serifs
},
describe('whitespace management', () => {
it('should remove whitespaces at start/end inside an element', () => {
- const ast = parse(`<div> <span/> </div>`)
+ const ast = baseParse(`<div> <span/> </div>`)
expect((ast.children[0] as ElementNode).children.length).toBe(1)
})
it('should remove whitespaces w/ newline between elements', () => {
- const ast = parse(`<div/> \n <div/> \n <div/>`)
+ const ast = baseParse(`<div/> \n <div/> \n <div/>`)
expect(ast.children.length).toBe(3)
expect(ast.children.every(c => c.type === NodeTypes.ELEMENT)).toBe(true)
})
it('should remove whitespaces adjacent to comments', () => {
- const ast = parse(`<div/> \n <!--foo--> <div/>`)
+ const ast = baseParse(`<div/> \n <!--foo--> <div/>`)
expect(ast.children.length).toBe(3)
expect(ast.children[0].type).toBe(NodeTypes.ELEMENT)
expect(ast.children[1].type).toBe(NodeTypes.COMMENT)
})
it('should remove whitespaces w/ newline between comments and elements', () => {
- const ast = parse(`<div/> \n <!--foo--> \n <div/>`)
+ const ast = baseParse(`<div/> \n <!--foo--> \n <div/>`)
expect(ast.children.length).toBe(3)
expect(ast.children[0].type).toBe(NodeTypes.ELEMENT)
expect(ast.children[1].type).toBe(NodeTypes.COMMENT)
})
it('should NOT remove whitespaces w/ newline between interpolations', () => {
- const ast = parse(`{{ foo }} \n {{ bar }}`)
+ const ast = baseParse(`{{ foo }} \n {{ bar }}`)
expect(ast.children.length).toBe(3)
expect(ast.children[0].type).toBe(NodeTypes.INTERPOLATION)
expect(ast.children[1]).toMatchObject({
})
it('should NOT remove whitespaces w/o newline between elements', () => {
- const ast = parse(`<div/> <div/> <div/>`)
+ const ast = baseParse(`<div/> <div/> <div/>`)
expect(ast.children.length).toBe(5)
expect(ast.children.map(c => c.type)).toMatchObject([
NodeTypes.ELEMENT,
})
it('should condense consecutive whitespaces in text', () => {
- const ast = parse(` foo \n bar baz `)
+ const ast = baseParse(` foo \n bar baz `)
expect((ast.children[0] as TextNode).content).toBe(` foo bar baz `)
})
})
),
() => {
const spy = jest.fn()
- const ast = parse(code, {
+ const ast = baseParse(code, {
getNamespace: (tag, parent) => {
const ns = parent ? parent.ns : Namespaces.HTML
if (ns === Namespaces.HTML) {
import { parse } from '../src'
import { mockWarn } from '@vue/runtime-test'
+import { baseParse, baseCompile } from '@vue/compiler-core'
describe('compiler:sfc', () => {
mockWarn()
describe('source map', () => {
test('style block', () => {
const style = parse(`<style>\n.color {\n color: red;\n }\n</style>\n`)
- .styles[0]
+ .descriptor.styles[0]
// TODO need to actually test this with SourceMapConsumer
expect(style.map).not.toBeUndefined()
})
test('script block', () => {
- const script = parse(`<script>\nconsole.log(1)\n }\n</script>\n`).script
+ const script = parse(`<script>\nconsole.log(1)\n }\n</script>\n`)
+ .descriptor.script
// TODO need to actually test this with SourceMapConsumer
expect(script!.map).not.toBeUndefined()
})
<style>
h1 { color: red }
</style>`
- const padFalse = parse(content.trim(), { pad: false })
+ const padFalse = parse(content.trim(), { pad: false }).descriptor
expect(padFalse.template!.content).toBe('\n<div></div>\n')
expect(padFalse.script!.content).toBe('\nexport default {}\n')
expect(padFalse.styles[0].content).toBe('\nh1 { color: red }\n')
- const padTrue = parse(content.trim(), { pad: true })
+ const padTrue = parse(content.trim(), { pad: true }).descriptor
expect(padTrue.script!.content).toBe(
Array(3 + 1).join('//\n') + '\nexport default {}\n'
)
Array(6 + 1).join('\n') + '\nh1 { color: red }\n'
)
- const padLine = parse(content.trim(), { pad: 'line' })
+ const padLine = parse(content.trim(), { pad: 'line' }).descriptor
expect(padLine.script!.content).toBe(
Array(3 + 1).join('//\n') + '\nexport default {}\n'
)
Array(6 + 1).join('\n') + '\nh1 { color: red }\n'
)
- const padSpace = parse(content.trim(), { pad: 'space' })
+ const padSpace = parse(content.trim(), { pad: 'space' }).descriptor
expect(padSpace.script!.content).toBe(
`<template>\n<div></div>\n</template>\n<script>`.replace(/./g, ' ') +
'\nexport default {}\n'
})
test('should ignore nodes with no content', () => {
- expect(parse(`<template/>`).template).toBe(null)
- expect(parse(`<script/>`).script).toBe(null)
- expect(parse(`<style/>`).styles.length).toBe(0)
- expect(parse(`<custom/>`).customBlocks.length).toBe(0)
+ expect(parse(`<template/>`).descriptor.template).toBe(null)
+ expect(parse(`<script/>`).descriptor.script).toBe(null)
+ expect(parse(`<style/>`).descriptor.styles.length).toBe(0)
+ expect(parse(`<custom/>`).descriptor.customBlocks.length).toBe(0)
})
- describe('error', () => {
+ test('nested templates', () => {
+ const content = `
+ <template v-if="ok">ok</template>
+ <div><div></div></div>
+ `
+ const sfc = parse(`<template>${content}</template>`).descriptor
+ expect(sfc.template!.content).toBe(content)
+ })
+
+ test('error tolerance', () => {
+ const { errors } = parse(`<template>`)
+ expect(errors.length).toBe(1)
+ })
+
+ test('should parse as DOM by default', () => {
+ const { errors } = parse(`<template><input></template>`)
+ expect(errors.length).toBe(0)
+ })
+
+ test('custom compiler', () => {
+ const { errors } = parse(`<template><input></template>`, {
+ compiler: {
+ parse: baseParse,
+ compile: baseCompile
+ }
+ })
+ expect(errors.length).toBe(1)
+ })
+
+ describe('warnings', () => {
test('should only allow single template element', () => {
parse(`<template><div/></template><template><div/></template>`)
expect(
import {
- parse as baseParse,
- TextModes,
NodeTypes,
- TextNode,
ElementNode,
- SourceLocation
+ SourceLocation,
+ CompilerError
} from '@vue/compiler-core'
import { RawSourceMap, SourceMapGenerator } from 'source-map'
import LRUCache from 'lru-cache'
import { generateCodeFrame } from '@vue/shared'
+import { TemplateCompiler } from './compileTemplate'
export interface SFCParseOptions {
filename?: string
sourceMap?: boolean
sourceRoot?: string
pad?: boolean | 'line' | 'space'
+ compiler?: TemplateCompiler
}
export interface SFCBlock {
customBlocks: SFCBlock[]
}
+export interface SFCParseResult {
+ descriptor: SFCDescriptor
+ errors: CompilerError[]
+}
+
const SFC_CACHE_MAX_SIZE = 500
-const sourceToSFC = new LRUCache<string, SFCDescriptor>(SFC_CACHE_MAX_SIZE)
+const sourceToSFC = new LRUCache<string, SFCParseResult>(SFC_CACHE_MAX_SIZE)
+
export function parse(
source: string,
{
sourceMap = true,
filename = 'component.vue',
sourceRoot = '',
- pad = false
+ pad = false,
+ compiler = require('@vue/compiler-dom')
}: SFCParseOptions = {}
-): SFCDescriptor {
- const sourceKey = source + sourceMap + filename + sourceRoot + pad
+): SFCParseResult {
+ const sourceKey =
+ source + sourceMap + filename + sourceRoot + pad + compiler.parse
const cache = sourceToSFC.get(sourceKey)
if (cache) {
return cache
}
- const sfc: SFCDescriptor = {
+ const descriptor: SFCDescriptor = {
filename,
template: null,
script: null,
customBlocks: []
}
- const ast = baseParse(source, {
+ const errors: CompilerError[] = []
+ const ast = compiler.parse(source, {
+ // there are no components at SFC parsing level
isNativeTag: () => true,
- getTextMode: () => TextModes.RAWTEXT
+ // preserve all whitespaces
+ isPreTag: () => true,
+ onError: e => {
+ errors.push(e)
+ }
})
ast.children.forEach(node => {
}
switch (node.tag) {
case 'template':
- if (!sfc.template) {
- sfc.template = createBlock(node, source, pad) as SFCTemplateBlock
+ if (!descriptor.template) {
+ descriptor.template = createBlock(
+ node,
+ source,
+ pad
+ ) as SFCTemplateBlock
} else {
warnDuplicateBlock(source, filename, node)
}
break
case 'script':
- if (!sfc.script) {
- sfc.script = createBlock(node, source, pad) as SFCScriptBlock
+ if (!descriptor.script) {
+ descriptor.script = createBlock(node, source, pad) as SFCScriptBlock
} else {
warnDuplicateBlock(source, filename, node)
}
break
case 'style':
- sfc.styles.push(createBlock(node, source, pad) as SFCStyleBlock)
+ descriptor.styles.push(createBlock(node, source, pad) as SFCStyleBlock)
break
default:
- sfc.customBlocks.push(createBlock(node, source, pad))
+ descriptor.customBlocks.push(createBlock(node, source, pad))
break
}
})
)
}
}
- genMap(sfc.template)
- genMap(sfc.script)
- sfc.styles.forEach(genMap)
+ genMap(descriptor.template)
+ genMap(descriptor.script)
+ descriptor.styles.forEach(genMap)
}
- sourceToSFC.set(sourceKey, sfc)
- return sfc
+ const result = {
+ descriptor,
+ errors
+ }
+ sourceToSFC.set(sourceKey, result)
+ return result
}
function warnDuplicateBlock(
pad: SFCParseOptions['pad']
): SFCBlock {
const type = node.tag
- const text = node.children[0] as TextNode
+ const start = node.children[0].loc.start
+ const end = node.children[node.children.length - 1].loc.end
+ const content = source.slice(start.offset, end.offset)
+ const loc = {
+ source: content,
+ start,
+ end
+ }
const attrs: Record<string, string | true> = {}
const block: SFCBlock = {
type,
- content: text.content,
- loc: text.loc,
+ content,
+ loc,
attrs
}
if (node.tag !== 'template' && pad) {