import { parse, ParserOptions } from '../src/parser'
import {
AttributeNode,
CommentNode,
DirectiveNode,
ElementNode,
ElementTypes,
ExpressionNode,
Namespaces,
NodeTypes,
Position,
TextNode
} from '../src/ast'
import { ParserErrorTypes } from '../src/errorTypes'
import { parserOptionsMinimal as parserOptions } from '../src/parserOptionsMinimal'
describe('parser/parse', () => {
describe('Text', () => {
test('simple text', () => {
const ast = parse('some text', parserOptions)
const text = ast.children[0] as TextNode
expect(text).toStrictEqual({
type: NodeTypes.TEXT,
content: 'some text',
isEmpty: false,
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 9, line: 1, column: 10 },
source: 'some text'
}
})
})
test('simple text with invalid end tag', () => {
const ast = parse('some text', {
...parserOptions,
onError: () => {}
})
const text = ast.children[0] as TextNode
expect(text).toStrictEqual({
type: NodeTypes.TEXT,
content: 'some text',
isEmpty: false,
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 9, line: 1, column: 10 },
source: 'some text'
}
})
})
test('text with interpolation', () => {
const ast = parse('some {{ foo + bar }} text', parserOptions)
const text1 = ast.children[0] as TextNode
const text2 = ast.children[2] as TextNode
expect(text1).toStrictEqual({
type: NodeTypes.TEXT,
content: 'some ',
isEmpty: false,
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 5, line: 1, column: 6 },
source: 'some '
}
})
expect(text2).toStrictEqual({
type: NodeTypes.TEXT,
content: ' text',
isEmpty: false,
loc: {
start: { offset: 20, line: 1, column: 21 },
end: { offset: 25, line: 1, column: 26 },
source: ' text'
}
})
})
test('text with interpolation which has `<`', () => {
const ast = parse('some {{ ad }} text', parserOptions)
const text1 = ast.children[0] as TextNode
const text2 = ast.children[2] as TextNode
expect(text1).toStrictEqual({
type: NodeTypes.TEXT,
content: 'some ',
isEmpty: false,
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 5, line: 1, column: 6 },
source: 'some '
}
})
expect(text2).toStrictEqual({
type: NodeTypes.TEXT,
content: ' text',
isEmpty: false,
loc: {
start: { offset: 21, line: 1, column: 22 },
end: { offset: 26, line: 1, column: 27 },
source: ' text'
}
})
})
test('text with mix of tags and interpolations', () => {
const ast = parse(
'some {{ foo < bar + foo }} text',
parserOptions
)
const text1 = ast.children[0] as TextNode
const text2 = (ast.children[1] as ElementNode).children![1] as TextNode
expect(text1).toStrictEqual({
type: NodeTypes.TEXT,
content: 'some ',
isEmpty: false,
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 5, line: 1, column: 6 },
source: 'some '
}
})
expect(text2).toStrictEqual({
type: NodeTypes.TEXT,
content: ' text',
isEmpty: false,
loc: {
start: { offset: 32, line: 1, column: 33 },
end: { offset: 37, line: 1, column: 38 },
source: ' text'
}
})
})
test('CDATA', () => {
const ast = parse('', parserOptions)
const text = (ast.children[0] as ElementNode).children![0] as TextNode
expect(text).toStrictEqual({
type: NodeTypes.TEXT,
content: 'some text',
isEmpty: false,
loc: {
start: { offset: 14, line: 1, column: 15 },
end: { offset: 23, line: 1, column: 24 },
source: 'some text'
}
})
})
test('lonly "<" don\'t separate nodes', () => {
const ast = parse('a < b', {
...parserOptions,
onError: errorCode => {
if (
errorCode !== ParserErrorTypes.INVALID_FIRST_CHARACTER_OF_TAG_NAME
) {
throw new Error(`${errorCode}`)
}
}
})
const text = ast.children[0] as TextNode
expect(text).toStrictEqual({
type: NodeTypes.TEXT,
content: 'a < b',
isEmpty: false,
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 5, line: 1, column: 6 },
source: 'a < b'
}
})
})
test('lonly "{{" don\'t separate nodes', () => {
const ast = parse('a {{ b', {
...parserOptions,
onError: errorCode => {
if (errorCode !== ParserErrorTypes.X_MISSING_INTERPOLATION_END) {
throw new Error(`${errorCode}`)
}
}
})
const text = ast.children[0] as TextNode
expect(text).toStrictEqual({
type: NodeTypes.TEXT,
content: 'a {{ b',
isEmpty: false,
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 6, line: 1, column: 7 },
source: 'a {{ b'
}
})
})
test('textarea handles comments/elements as just a text', () => {
const ast = parse(
'',
parserOptions
)
const element = ast.children[0] as ElementNode
const text = element.children[0] as TextNode
expect(text).toStrictEqual({
type: NodeTypes.TEXT,
content: 'someafter', parserOptions)
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
type: NodeTypes.ELEMENT,
ns: Namespaces.HTML,
tag: 'img',
tagType: ElementTypes.ELEMENT,
props: [],
isSelfClosing: false,
children: [],
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 5, line: 1, column: 6 },
source: '
'
}
})
})
test('attribute with no value', () => {
const ast = parse('', parserOptions)
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
type: NodeTypes.ELEMENT,
ns: Namespaces.HTML,
tag: 'div',
tagType: ElementTypes.ELEMENT,
props: [
{
type: NodeTypes.ATTRIBUTE,
name: 'id',
value: undefined,
loc: {
start: { offset: 5, line: 1, column: 6 },
end: { offset: 7, line: 1, column: 8 },
source: 'id'
}
}
],
isSelfClosing: false,
children: [],
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 14, line: 1, column: 15 },
source: ''
}
})
})
test('attribute with empty value, double quote', () => {
const ast = parse('', parserOptions)
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
type: NodeTypes.ELEMENT,
ns: Namespaces.HTML,
tag: 'div',
tagType: ElementTypes.ELEMENT,
props: [
{
type: NodeTypes.ATTRIBUTE,
name: 'id',
value: {
type: NodeTypes.TEXT,
content: '',
isEmpty: true,
loc: {
start: { offset: 8, line: 1, column: 9 },
end: { offset: 10, line: 1, column: 11 },
source: '""'
}
},
loc: {
start: { offset: 5, line: 1, column: 6 },
end: { offset: 10, line: 1, column: 11 },
source: 'id=""'
}
}
],
isSelfClosing: false,
children: [],
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 17, line: 1, column: 18 },
source: ''
}
})
})
test('attribute with empty value, single quote', () => {
const ast = parse("", parserOptions)
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
type: NodeTypes.ELEMENT,
ns: Namespaces.HTML,
tag: 'div',
tagType: ElementTypes.ELEMENT,
props: [
{
type: NodeTypes.ATTRIBUTE,
name: 'id',
value: {
type: NodeTypes.TEXT,
content: '',
isEmpty: true,
loc: {
start: { offset: 8, line: 1, column: 9 },
end: { offset: 10, line: 1, column: 11 },
source: "''"
}
},
loc: {
start: { offset: 5, line: 1, column: 6 },
end: { offset: 10, line: 1, column: 11 },
source: "id=''"
}
}
],
isSelfClosing: false,
children: [],
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 17, line: 1, column: 18 },
source: ""
}
})
})
test('attribute with value, double quote', () => {
const ast = parse('', parserOptions)
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
type: NodeTypes.ELEMENT,
ns: Namespaces.HTML,
tag: 'div',
tagType: ElementTypes.ELEMENT,
props: [
{
type: NodeTypes.ATTRIBUTE,
name: 'id',
value: {
type: NodeTypes.TEXT,
content: ">'",
isEmpty: false,
loc: {
start: { offset: 8, line: 1, column: 9 },
end: { offset: 12, line: 1, column: 13 },
source: '">\'"'
}
},
loc: {
start: { offset: 5, line: 1, column: 6 },
end: { offset: 12, line: 1, column: 13 },
source: 'id=">\'"'
}
}
],
isSelfClosing: false,
children: [],
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 19, line: 1, column: 20 },
source: ''
}
})
})
test('attribute with value, single quote', () => {
const ast = parse("", parserOptions)
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
type: NodeTypes.ELEMENT,
ns: Namespaces.HTML,
tag: 'div',
tagType: ElementTypes.ELEMENT,
props: [
{
type: NodeTypes.ATTRIBUTE,
name: 'id',
value: {
type: NodeTypes.TEXT,
content: '>"',
isEmpty: false,
loc: {
start: { offset: 8, line: 1, column: 9 },
end: { offset: 12, line: 1, column: 13 },
source: "'>\"'"
}
},
loc: {
start: { offset: 5, line: 1, column: 6 },
end: { offset: 12, line: 1, column: 13 },
source: "id='>\"'"
}
}
],
isSelfClosing: false,
children: [],
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 19, line: 1, column: 20 },
source: ""
}
})
})
test('attribute with value, unquoted', () => {
const ast = parse('', parserOptions)
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
type: NodeTypes.ELEMENT,
ns: Namespaces.HTML,
tag: 'div',
tagType: ElementTypes.ELEMENT,
props: [
{
type: NodeTypes.ATTRIBUTE,
name: 'id',
value: {
type: NodeTypes.TEXT,
content: 'a/',
isEmpty: false,
loc: {
start: { offset: 8, line: 1, column: 9 },
end: { offset: 10, line: 1, column: 11 },
source: 'a/'
}
},
loc: {
start: { offset: 5, line: 1, column: 6 },
end: { offset: 10, line: 1, column: 11 },
source: 'id=a/'
}
}
],
isSelfClosing: false,
children: [],
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 17, line: 1, column: 18 },
source: ''
}
})
})
test('multiple attributes', () => {
const ast = parse(
'',
parserOptions
)
const element = ast.children[0] as ElementNode
expect(element).toStrictEqual({
type: NodeTypes.ELEMENT,
ns: Namespaces.HTML,
tag: 'div',
tagType: ElementTypes.ELEMENT,
props: [
{
type: NodeTypes.ATTRIBUTE,
name: 'id',
value: {
type: NodeTypes.TEXT,
content: 'a',
isEmpty: false,
loc: {
start: { offset: 8, line: 1, column: 9 },
end: { offset: 9, line: 1, column: 10 },
source: 'a'
}
},
loc: {
start: { offset: 5, line: 1, column: 6 },
end: { offset: 9, line: 1, column: 10 },
source: 'id=a'
}
},
{
type: NodeTypes.ATTRIBUTE,
name: 'class',
value: {
type: NodeTypes.TEXT,
content: 'c',
isEmpty: false,
loc: {
start: { offset: 16, line: 1, column: 17 },
end: { offset: 19, line: 1, column: 20 },
source: '"c"'
}
},
loc: {
start: { offset: 10, line: 1, column: 11 },
end: { offset: 19, line: 1, column: 20 },
source: 'class="c"'
}
},
{
type: NodeTypes.ATTRIBUTE,
name: 'inert',
value: undefined,
loc: {
start: { offset: 20, line: 1, column: 21 },
end: { offset: 25, line: 1, column: 26 },
source: 'inert'
}
},
{
type: NodeTypes.ATTRIBUTE,
name: 'style',
value: {
type: NodeTypes.TEXT,
content: '',
isEmpty: true,
loc: {
start: { offset: 32, line: 1, column: 33 },
end: { offset: 34, line: 1, column: 35 },
source: "''"
}
},
loc: {
start: { offset: 26, line: 1, column: 27 },
end: { offset: 34, line: 1, column: 35 },
source: "style=''"
}
}
],
isSelfClosing: false,
children: [],
loc: {
start: { offset: 0, line: 1, column: 1 },
end: { offset: 41, line: 1, column: 42 },
source: ''
}
})
})
test('directive with no value', () => {
const ast = parse('', parserOptions)
const directive = (ast.children[0] as ElementNode)
.props[0] as DirectiveNode
expect(directive).toStrictEqual({
type: NodeTypes.DIRECTIVE,
name: 'if',
arg: undefined,
modifiers: [],
exp: undefined,
loc: {
start: { offset: 5, line: 1, column: 6 },
end: { offset: 9, line: 1, column: 10 },
source: 'v-if'
}
})
})
test('directive with value', () => {
const ast = parse('', parserOptions)
const directive = (ast.children[0] as ElementNode)
.props[0] as DirectiveNode
expect(directive).toStrictEqual({
type: NodeTypes.DIRECTIVE,
name: 'if',
arg: undefined,
modifiers: [],
exp: {
type: NodeTypes.EXPRESSION,
content: 'a',
isStatic: false,
loc: {
start: { offset: 10, line: 1, column: 11 },
end: { offset: 13, line: 1, column: 14 },
source: '"a"'
}
},
loc: {
start: { offset: 5, line: 1, column: 6 },
end: { offset: 13, line: 1, column: 14 },
source: 'v-if="a"'
}
})
})
test('directive with argument', () => {
const ast = parse('', parserOptions)
const directive = (ast.children[0] as ElementNode)
.props[0] as DirectiveNode
expect(directive).toStrictEqual({
type: NodeTypes.DIRECTIVE,
name: 'on',
arg: {
type: 4,
content: 'click',
isStatic: true,
loc: {
source: 'click',
start: {
column: 11,
line: 1,
offset: 10
},
end: {
column: 16,
line: 1,
offset: 15
}
}
},
modifiers: [],
exp: undefined,
loc: {
start: { offset: 5, line: 1, column: 6 },
end: { offset: 15, line: 1, column: 16 },
source: 'v-on:click'
}
})
})
test('directive with a modifier', () => {
const ast = parse('', parserOptions)
const directive = (ast.children[0] as ElementNode)
.props[0] as DirectiveNode
expect(directive).toStrictEqual({
type: NodeTypes.DIRECTIVE,
name: 'on',
arg: undefined,
modifiers: ['enter'],
exp: undefined,
loc: {
start: { offset: 5, line: 1, column: 6 },
end: { offset: 15, line: 1, column: 16 },
source: 'v-on.enter'
}
})
})
test('directive with two modifiers', () => {
const ast = parse('', parserOptions)
const directive = (ast.children[0] as ElementNode)
.props[0] as DirectiveNode
expect(directive).toStrictEqual({
type: NodeTypes.DIRECTIVE,
name: 'on',
arg: undefined,
modifiers: ['enter', 'exact'],
exp: undefined,
loc: {
start: { offset: 5, line: 1, column: 6 },
end: { offset: 21, line: 1, column: 22 },
source: 'v-on.enter.exact'
}
})
})
test('directive with argument and modifiers', () => {
const ast = parse('', parserOptions)
const directive = (ast.children[0] as ElementNode)
.props[0] as DirectiveNode
expect(directive).toStrictEqual({
type: NodeTypes.DIRECTIVE,
name: 'on',
arg: {
type: 4,
content: 'click',
isStatic: true,
loc: {
source: 'click',
start: {
column: 11,
line: 1,
offset: 10
},
end: {
column: 16,
line: 1,
offset: 15
}
}
},
modifiers: ['enter', 'exact'],
exp: undefined,
loc: {
start: { offset: 5, line: 1, column: 6 },
end: { offset: 27, line: 1, column: 28 },
source: 'v-on:click.enter.exact'
}
})
})
test('v-bind shorthand', () => {
const ast = parse('', parserOptions)
const directive = (ast.children[0] as ElementNode)
.props[0] as DirectiveNode
expect(directive).toStrictEqual({
type: NodeTypes.DIRECTIVE,
name: 'bind',
arg: {
type: 4,
content: 'a',
isStatic: true,
loc: {
source: 'a',
start: {
column: 7,
line: 1,
offset: 6
},
end: {
column: 8,
line: 1,
offset: 7
}
}
},
modifiers: [],
exp: {
type: NodeTypes.EXPRESSION,
content: 'b',
isStatic: false,
loc: {
start: { offset: 8, line: 1, column: 9 },
end: { offset: 9, line: 1, column: 10 },
source: 'b'
}
},
loc: {
start: { offset: 5, line: 1, column: 6 },
end: { offset: 9, line: 1, column: 10 },
source: ':a=b'
}
})
})
test('v-bind shorthand with modifier', () => {
const ast = parse('', parserOptions)
const directive = (ast.children[0] as ElementNode)
.props[0] as DirectiveNode
expect(directive).toStrictEqual({
type: NodeTypes.DIRECTIVE,
name: 'bind',
arg: {
type: 4,
content: 'a',
isStatic: true,
loc: {
source: 'a',
start: {
column: 7,
line: 1,
offset: 6
},
end: {
column: 8,
line: 1,
offset: 7
}
}
},
modifiers: ['sync'],
exp: {
type: NodeTypes.EXPRESSION,
content: 'b',
isStatic: false,
loc: {
start: { offset: 13, line: 1, column: 14 },
end: { offset: 14, line: 1, column: 15 },
source: 'b'
}
},
loc: {
start: { offset: 5, line: 1, column: 6 },
end: { offset: 14, line: 1, column: 15 },
source: ':a.sync=b'
}
})
})
test('v-on shorthand', () => {
const ast = parse('', parserOptions)
const directive = (ast.children[0] as ElementNode)
.props[0] as DirectiveNode
expect(directive).toStrictEqual({
type: NodeTypes.DIRECTIVE,
name: 'on',
arg: {
type: 4,
content: 'a',
isStatic: true,
loc: {
source: 'a',
start: {
column: 7,
line: 1,
offset: 6
},
end: {
column: 8,
line: 1,
offset: 7
}
}
},
modifiers: [],
exp: {
type: NodeTypes.EXPRESSION,
content: 'b',
isStatic: false,
loc: {
start: { offset: 8, line: 1, column: 9 },
end: { offset: 9, line: 1, column: 10 },
source: 'b'
}
},
loc: {
start: { offset: 5, line: 1, column: 6 },
end: { offset: 9, line: 1, column: 10 },
source: '@a=b'
}
})
})
test('v-on shorthand with modifier', () => {
const ast = parse('', parserOptions)
const directive = (ast.children[0] as ElementNode)
.props[0] as DirectiveNode
expect(directive).toStrictEqual({
type: NodeTypes.DIRECTIVE,
name: 'on',
arg: {
type: 4,
content: 'a',
isStatic: true,
loc: {
source: 'a',
start: {
column: 7,
line: 1,
offset: 6
},
end: {
column: 8,
line: 1,
offset: 7
}
}
},
modifiers: ['enter'],
exp: {
type: NodeTypes.EXPRESSION,
content: 'b',
isStatic: false,
loc: {
start: { offset: 14, line: 1, column: 15 },
end: { offset: 15, line: 1, column: 16 },
source: 'b'
}
},
loc: {
start: { offset: 5, line: 1, column: 6 },
end: { offset: 15, line: 1, column: 16 },
source: '@a.enter=b'
}
})
})
test('end tags are case-insensitive.', () => {
const ast = parse('