feat(compiler-core): add parser transform for v-for directive (#65)
* feat(compiler-core): add parser transform for v-for directive * fix: Include source location for expressions * chore: remove comment * refactor(compiler-core): extract hepler functions to utils
This commit is contained in:
parent
bbb57c26a2
commit
10c1a2b332
480
packages/compiler-core/__tests__/directives/vFor.spec.ts
Normal file
480
packages/compiler-core/__tests__/directives/vFor.spec.ts
Normal file
@ -0,0 +1,480 @@
|
|||||||
|
import { parse } from '../../src/parse'
|
||||||
|
import { transform } from '../../src/transform'
|
||||||
|
import { transformFor } from '../../src/directives/vFor'
|
||||||
|
import { ForNode, NodeTypes } from '../../src/ast'
|
||||||
|
import { ErrorCodes } from '../../src/errors'
|
||||||
|
|
||||||
|
describe('v-for', () => {
|
||||||
|
test('number expression', () => {
|
||||||
|
const node = parse('<span v-for="index in 5" />')
|
||||||
|
|
||||||
|
transform(node, { transforms: [transformFor] })
|
||||||
|
|
||||||
|
expect(node.children.length).toBe(1)
|
||||||
|
|
||||||
|
const forNode = node.children[0] as ForNode
|
||||||
|
|
||||||
|
expect(forNode.type).toBe(NodeTypes.FOR)
|
||||||
|
expect(forNode.keyAlias).toBeUndefined()
|
||||||
|
expect(forNode.objectIndexAlias).toBeUndefined()
|
||||||
|
expect(forNode.valueAlias!.content).toBe('index')
|
||||||
|
expect(forNode.source.content).toBe('5')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('value', () => {
|
||||||
|
const node = parse('<span v-for="(item) in items" />')
|
||||||
|
|
||||||
|
transform(node, { transforms: [transformFor] })
|
||||||
|
|
||||||
|
expect(node.children.length).toBe(1)
|
||||||
|
|
||||||
|
const forNode = node.children[0] as ForNode
|
||||||
|
|
||||||
|
expect(forNode.type).toBe(NodeTypes.FOR)
|
||||||
|
expect(forNode.keyAlias).toBeUndefined()
|
||||||
|
expect(forNode.objectIndexAlias).toBeUndefined()
|
||||||
|
expect(forNode.valueAlias!.content).toBe('item')
|
||||||
|
expect(forNode.source.content).toBe('items')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('object de-structured value', () => {
|
||||||
|
const node = parse('<span v-for="({ id, value }) in items" />')
|
||||||
|
|
||||||
|
transform(node, { transforms: [transformFor] })
|
||||||
|
|
||||||
|
expect(node.children.length).toBe(1)
|
||||||
|
|
||||||
|
const forNode = node.children[0] as ForNode
|
||||||
|
|
||||||
|
expect(forNode.type).toBe(NodeTypes.FOR)
|
||||||
|
expect(forNode.keyAlias).toBeUndefined()
|
||||||
|
expect(forNode.objectIndexAlias).toBeUndefined()
|
||||||
|
expect(forNode.valueAlias!.content).toBe('{ id, value }')
|
||||||
|
expect(forNode.source.content).toBe('items')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('array de-structured value', () => {
|
||||||
|
const node = parse('<span v-for="([ id, value ]) in items" />')
|
||||||
|
|
||||||
|
transform(node, { transforms: [transformFor] })
|
||||||
|
|
||||||
|
expect(node.children.length).toBe(1)
|
||||||
|
|
||||||
|
const forNode = node.children[0] as ForNode
|
||||||
|
|
||||||
|
expect(forNode.type).toBe(NodeTypes.FOR)
|
||||||
|
expect(forNode.keyAlias).toBeUndefined()
|
||||||
|
expect(forNode.objectIndexAlias).toBeUndefined()
|
||||||
|
expect(forNode.valueAlias!.content).toBe('[ id, value ]')
|
||||||
|
expect(forNode.source.content).toBe('items')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('value and key', () => {
|
||||||
|
const node = parse('<span v-for="(item, key) in items" />')
|
||||||
|
|
||||||
|
transform(node, { transforms: [transformFor] })
|
||||||
|
|
||||||
|
expect(node.children.length).toBe(1)
|
||||||
|
|
||||||
|
const forNode = node.children[0] as ForNode
|
||||||
|
|
||||||
|
expect(forNode.keyAlias).not.toBeUndefined()
|
||||||
|
expect(forNode.keyAlias!.content).toBe('key')
|
||||||
|
expect(forNode.objectIndexAlias).toBeUndefined()
|
||||||
|
expect(forNode.valueAlias!.content).toBe('item')
|
||||||
|
expect(forNode.source.content).toBe('items')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('value, key and index', () => {
|
||||||
|
const node = parse('<span v-for="(value, key, index) in items" />')
|
||||||
|
|
||||||
|
transform(node, { transforms: [transformFor] })
|
||||||
|
|
||||||
|
expect(node.children.length).toBe(1)
|
||||||
|
|
||||||
|
const forNode = node.children[0] as ForNode
|
||||||
|
|
||||||
|
expect(forNode.type).toBe(NodeTypes.FOR)
|
||||||
|
expect(forNode.keyAlias).not.toBeUndefined()
|
||||||
|
expect(forNode.keyAlias!.content).toBe('key')
|
||||||
|
expect(forNode.objectIndexAlias).not.toBeUndefined()
|
||||||
|
expect(forNode.objectIndexAlias!.content).toBe('index')
|
||||||
|
expect(forNode.valueAlias!.content).toBe('value')
|
||||||
|
expect(forNode.source.content).toBe('items')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('skipped key', () => {
|
||||||
|
const node = parse('<span v-for="(value,,index) in items" />')
|
||||||
|
|
||||||
|
transform(node, { transforms: [transformFor] })
|
||||||
|
|
||||||
|
expect(node.children.length).toBe(1)
|
||||||
|
|
||||||
|
const forNode = node.children[0] as ForNode
|
||||||
|
|
||||||
|
expect(forNode.type).toBe(NodeTypes.FOR)
|
||||||
|
expect(forNode.keyAlias).toBeUndefined()
|
||||||
|
expect(forNode.objectIndexAlias).not.toBeUndefined()
|
||||||
|
expect(forNode.objectIndexAlias!.content).toBe('index')
|
||||||
|
expect(forNode.valueAlias!.content).toBe('value')
|
||||||
|
expect(forNode.source.content).toBe('items')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('skipped value and key', () => {
|
||||||
|
const node = parse('<span v-for="(,,index) in items" />')
|
||||||
|
|
||||||
|
transform(node, { transforms: [transformFor] })
|
||||||
|
|
||||||
|
expect(node.children.length).toBe(1)
|
||||||
|
|
||||||
|
const forNode = node.children[0] as ForNode
|
||||||
|
|
||||||
|
expect(forNode.type).toBe(NodeTypes.FOR)
|
||||||
|
expect(forNode.keyAlias).toBeUndefined()
|
||||||
|
expect(forNode.objectIndexAlias).not.toBeUndefined()
|
||||||
|
expect(forNode.objectIndexAlias!.content).toBe('index')
|
||||||
|
expect(forNode.valueAlias).toBeUndefined()
|
||||||
|
expect(forNode.source.content).toBe('items')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('unbracketed value', () => {
|
||||||
|
const node = parse('<span v-for="item in items" />')
|
||||||
|
|
||||||
|
transform(node, { transforms: [transformFor] })
|
||||||
|
|
||||||
|
expect(node.children.length).toBe(1)
|
||||||
|
|
||||||
|
const forNode = node.children[0] as ForNode
|
||||||
|
|
||||||
|
expect(forNode.type).toBe(NodeTypes.FOR)
|
||||||
|
expect(forNode.keyAlias).toBeUndefined()
|
||||||
|
expect(forNode.objectIndexAlias).toBeUndefined()
|
||||||
|
expect(forNode.valueAlias!.content).toBe('item')
|
||||||
|
expect(forNode.source.content).toBe('items')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('unbracketed value and key', () => {
|
||||||
|
const node = parse('<span v-for="item, key in items" />')
|
||||||
|
|
||||||
|
transform(node, { transforms: [transformFor] })
|
||||||
|
|
||||||
|
expect(node.children.length).toBe(1)
|
||||||
|
|
||||||
|
const forNode = node.children[0] as ForNode
|
||||||
|
|
||||||
|
expect(forNode.type).toBe(NodeTypes.FOR)
|
||||||
|
expect(forNode.keyAlias).not.toBeUndefined()
|
||||||
|
expect(forNode.keyAlias!.content).toBe('key')
|
||||||
|
expect(forNode.objectIndexAlias).toBeUndefined()
|
||||||
|
expect(forNode.valueAlias!.content).toBe('item')
|
||||||
|
expect(forNode.source.content).toBe('items')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('unbracketed value, key and index', () => {
|
||||||
|
const node = parse('<span v-for="value, key, index in items" />')
|
||||||
|
|
||||||
|
transform(node, { transforms: [transformFor] })
|
||||||
|
|
||||||
|
expect(node.children.length).toBe(1)
|
||||||
|
|
||||||
|
const forNode = node.children[0] as ForNode
|
||||||
|
|
||||||
|
expect(forNode.type).toBe(NodeTypes.FOR)
|
||||||
|
expect(forNode.keyAlias).not.toBeUndefined()
|
||||||
|
expect(forNode.keyAlias!.content).toBe('key')
|
||||||
|
expect(forNode.objectIndexAlias).not.toBeUndefined()
|
||||||
|
expect(forNode.objectIndexAlias!.content).toBe('index')
|
||||||
|
expect(forNode.valueAlias!.content).toBe('value')
|
||||||
|
expect(forNode.source.content).toBe('items')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('unbracketed skipped key', () => {
|
||||||
|
const node = parse('<span v-for="value, , index in items" />')
|
||||||
|
|
||||||
|
transform(node, { transforms: [transformFor] })
|
||||||
|
|
||||||
|
expect(node.children.length).toBe(1)
|
||||||
|
|
||||||
|
const forNode = node.children[0] as ForNode
|
||||||
|
|
||||||
|
expect(forNode.type).toBe(NodeTypes.FOR)
|
||||||
|
expect(forNode.keyAlias).toBeUndefined()
|
||||||
|
expect(forNode.objectIndexAlias).not.toBeUndefined()
|
||||||
|
expect(forNode.objectIndexAlias!.content).toBe('index')
|
||||||
|
expect(forNode.valueAlias!.content).toBe('value')
|
||||||
|
expect(forNode.source.content).toBe('items')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('unbracketed skipped value and key', () => {
|
||||||
|
const node = parse('<span v-for=", , index in items" />')
|
||||||
|
|
||||||
|
transform(node, { transforms: [transformFor] })
|
||||||
|
|
||||||
|
expect(node.children.length).toBe(1)
|
||||||
|
|
||||||
|
const forNode = node.children[0] as ForNode
|
||||||
|
|
||||||
|
expect(forNode.type).toBe(NodeTypes.FOR)
|
||||||
|
expect(forNode.keyAlias).toBeUndefined()
|
||||||
|
expect(forNode.objectIndexAlias).not.toBeUndefined()
|
||||||
|
expect(forNode.objectIndexAlias!.content).toBe('index')
|
||||||
|
expect(forNode.valueAlias).toBeUndefined()
|
||||||
|
expect(forNode.source.content).toBe('items')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('missing expression', () => {
|
||||||
|
const node = parse('<span v-for />')
|
||||||
|
const onError = jest.fn()
|
||||||
|
transform(node, { transforms: [transformFor], onError })
|
||||||
|
|
||||||
|
expect(onError).toHaveBeenCalledTimes(1)
|
||||||
|
expect(onError).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
code: ErrorCodes.X_FOR_NO_EXPRESSION
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('empty expression', () => {
|
||||||
|
const node = parse('<span v-for="" />')
|
||||||
|
const onError = jest.fn()
|
||||||
|
transform(node, { transforms: [transformFor], onError })
|
||||||
|
|
||||||
|
expect(onError).toHaveBeenCalledTimes(1)
|
||||||
|
expect(onError).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
code: ErrorCodes.X_FOR_MALFORMED_EXPRESSION
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('invalid expression', () => {
|
||||||
|
const node = parse('<span v-for="items" />')
|
||||||
|
const onError = jest.fn()
|
||||||
|
transform(node, { transforms: [transformFor], onError })
|
||||||
|
|
||||||
|
expect(onError).toHaveBeenCalledTimes(1)
|
||||||
|
expect(onError).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
code: ErrorCodes.X_FOR_MALFORMED_EXPRESSION
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('missing source', () => {
|
||||||
|
const node = parse('<span v-for="item in" />')
|
||||||
|
const onError = jest.fn()
|
||||||
|
transform(node, { transforms: [transformFor], onError })
|
||||||
|
|
||||||
|
expect(onError).toHaveBeenCalledTimes(1)
|
||||||
|
expect(onError).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
code: ErrorCodes.X_FOR_MALFORMED_EXPRESSION
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('missing value', () => {
|
||||||
|
const node = parse('<span v-for="in items" />')
|
||||||
|
const onError = jest.fn()
|
||||||
|
transform(node, { transforms: [transformFor], onError })
|
||||||
|
|
||||||
|
expect(onError).toHaveBeenCalledTimes(1)
|
||||||
|
expect(onError).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
code: ErrorCodes.X_FOR_MALFORMED_EXPRESSION
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('source location', () => {
|
||||||
|
test('value & source', () => {
|
||||||
|
const source = '<span v-for="item in items" />'
|
||||||
|
const node = parse(source)
|
||||||
|
|
||||||
|
transform(node, { transforms: [transformFor] })
|
||||||
|
|
||||||
|
expect(node.children.length).toBe(1)
|
||||||
|
|
||||||
|
const forNode = node.children[0] as ForNode
|
||||||
|
|
||||||
|
expect(forNode.type).toBe(NodeTypes.FOR)
|
||||||
|
|
||||||
|
expect(forNode.valueAlias!.content).toBe('item')
|
||||||
|
expect(forNode.valueAlias!.loc.start.offset).toBe(
|
||||||
|
source.indexOf('item') - 1
|
||||||
|
)
|
||||||
|
expect(forNode.valueAlias!.loc.start.line).toBe(1)
|
||||||
|
expect(forNode.valueAlias!.loc.start.column).toBe(source.indexOf('item'))
|
||||||
|
expect(forNode.valueAlias!.loc.end.line).toBe(1)
|
||||||
|
expect(forNode.valueAlias!.loc.end.column).toBe(
|
||||||
|
source.indexOf('item') + 4
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(forNode.source.content).toBe('items')
|
||||||
|
expect(forNode.source.loc.start.offset).toBe(source.indexOf('items') - 1)
|
||||||
|
expect(forNode.source.loc.start.line).toBe(1)
|
||||||
|
expect(forNode.source.loc.start.column).toBe(source.indexOf('items'))
|
||||||
|
expect(forNode.source.loc.end.line).toBe(1)
|
||||||
|
expect(forNode.source.loc.end.column).toBe(source.indexOf('items') + 5)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('bracketed value', () => {
|
||||||
|
const source = '<span v-for="( item ) in items" />'
|
||||||
|
const node = parse(source)
|
||||||
|
|
||||||
|
transform(node, { transforms: [transformFor] })
|
||||||
|
|
||||||
|
expect(node.children.length).toBe(1)
|
||||||
|
|
||||||
|
const forNode = node.children[0] as ForNode
|
||||||
|
|
||||||
|
expect(forNode.type).toBe(NodeTypes.FOR)
|
||||||
|
|
||||||
|
expect(forNode.valueAlias!.content).toBe('item')
|
||||||
|
expect(forNode.valueAlias!.loc.start.offset).toBe(
|
||||||
|
source.indexOf('item') - 1
|
||||||
|
)
|
||||||
|
expect(forNode.valueAlias!.loc.start.line).toBe(1)
|
||||||
|
expect(forNode.valueAlias!.loc.start.column).toBe(source.indexOf('item'))
|
||||||
|
expect(forNode.valueAlias!.loc.end.line).toBe(1)
|
||||||
|
expect(forNode.valueAlias!.loc.end.column).toBe(
|
||||||
|
source.indexOf('item') + 4
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(forNode.source.content).toBe('items')
|
||||||
|
expect(forNode.source.loc.start.offset).toBe(source.indexOf('items') - 1)
|
||||||
|
expect(forNode.source.loc.start.line).toBe(1)
|
||||||
|
expect(forNode.source.loc.start.column).toBe(source.indexOf('items'))
|
||||||
|
expect(forNode.source.loc.end.line).toBe(1)
|
||||||
|
expect(forNode.source.loc.end.column).toBe(source.indexOf('items') + 5)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('de-structured value', () => {
|
||||||
|
const source = '<span v-for="( { id, key })in items" />'
|
||||||
|
const node = parse(source)
|
||||||
|
|
||||||
|
transform(node, { transforms: [transformFor] })
|
||||||
|
|
||||||
|
expect(node.children.length).toBe(1)
|
||||||
|
|
||||||
|
const forNode = node.children[0] as ForNode
|
||||||
|
|
||||||
|
expect(forNode.type).toBe(NodeTypes.FOR)
|
||||||
|
|
||||||
|
expect(forNode.valueAlias!.content).toBe('{ id, key }')
|
||||||
|
expect(forNode.valueAlias!.loc.start.offset).toBe(
|
||||||
|
source.indexOf('{ id, key }') - 1
|
||||||
|
)
|
||||||
|
expect(forNode.valueAlias!.loc.start.line).toBe(1)
|
||||||
|
expect(forNode.valueAlias!.loc.start.column).toBe(
|
||||||
|
source.indexOf('{ id, key }')
|
||||||
|
)
|
||||||
|
expect(forNode.valueAlias!.loc.end.line).toBe(1)
|
||||||
|
expect(forNode.valueAlias!.loc.end.column).toBe(
|
||||||
|
source.indexOf('{ id, key }') + '{ id, key }'.length
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(forNode.source.content).toBe('items')
|
||||||
|
expect(forNode.source.loc.start.offset).toBe(source.indexOf('items') - 1)
|
||||||
|
expect(forNode.source.loc.start.line).toBe(1)
|
||||||
|
expect(forNode.source.loc.start.column).toBe(source.indexOf('items'))
|
||||||
|
expect(forNode.source.loc.end.line).toBe(1)
|
||||||
|
expect(forNode.source.loc.end.column).toBe(source.indexOf('items') + 5)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('bracketed value, key, index', () => {
|
||||||
|
const source = '<span v-for="( item, key, index ) in items" />'
|
||||||
|
const node = parse(source)
|
||||||
|
|
||||||
|
transform(node, { transforms: [transformFor] })
|
||||||
|
|
||||||
|
expect(node.children.length).toBe(1)
|
||||||
|
|
||||||
|
const forNode = node.children[0] as ForNode
|
||||||
|
|
||||||
|
expect(forNode.type).toBe(NodeTypes.FOR)
|
||||||
|
|
||||||
|
expect(forNode.valueAlias!.content).toBe('item')
|
||||||
|
expect(forNode.valueAlias!.loc.start.offset).toBe(
|
||||||
|
source.indexOf('item') - 1
|
||||||
|
)
|
||||||
|
expect(forNode.valueAlias!.loc.start.line).toBe(1)
|
||||||
|
expect(forNode.valueAlias!.loc.start.column).toBe(source.indexOf('item'))
|
||||||
|
expect(forNode.valueAlias!.loc.end.line).toBe(1)
|
||||||
|
expect(forNode.valueAlias!.loc.end.column).toBe(
|
||||||
|
source.indexOf('item') + 4
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(forNode.keyAlias!.content).toBe('key')
|
||||||
|
expect(forNode.keyAlias!.loc.start.offset).toBe(source.indexOf('key') - 1)
|
||||||
|
expect(forNode.keyAlias!.loc.start.line).toBe(1)
|
||||||
|
expect(forNode.keyAlias!.loc.start.column).toBe(source.indexOf('key'))
|
||||||
|
expect(forNode.keyAlias!.loc.end.line).toBe(1)
|
||||||
|
expect(forNode.keyAlias!.loc.end.column).toBe(source.indexOf('key') + 3)
|
||||||
|
|
||||||
|
expect(forNode.objectIndexAlias!.content).toBe('index')
|
||||||
|
expect(forNode.objectIndexAlias!.loc.start.offset).toBe(
|
||||||
|
source.indexOf('index') - 1
|
||||||
|
)
|
||||||
|
expect(forNode.objectIndexAlias!.loc.start.line).toBe(1)
|
||||||
|
expect(forNode.objectIndexAlias!.loc.start.column).toBe(
|
||||||
|
source.indexOf('index')
|
||||||
|
)
|
||||||
|
expect(forNode.objectIndexAlias!.loc.end.line).toBe(1)
|
||||||
|
expect(forNode.objectIndexAlias!.loc.end.column).toBe(
|
||||||
|
source.indexOf('index') + 5
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(forNode.source.content).toBe('items')
|
||||||
|
expect(forNode.source.loc.start.offset).toBe(source.indexOf('items') - 1)
|
||||||
|
expect(forNode.source.loc.start.line).toBe(1)
|
||||||
|
expect(forNode.source.loc.start.column).toBe(source.indexOf('items'))
|
||||||
|
expect(forNode.source.loc.end.line).toBe(1)
|
||||||
|
expect(forNode.source.loc.end.column).toBe(source.indexOf('items') + 5)
|
||||||
|
})
|
||||||
|
test('skipped key', () => {
|
||||||
|
const source = '<span v-for="( item,, index ) in items" />'
|
||||||
|
const node = parse(source)
|
||||||
|
|
||||||
|
transform(node, { transforms: [transformFor] })
|
||||||
|
|
||||||
|
expect(node.children.length).toBe(1)
|
||||||
|
|
||||||
|
const forNode = node.children[0] as ForNode
|
||||||
|
|
||||||
|
expect(forNode.type).toBe(NodeTypes.FOR)
|
||||||
|
|
||||||
|
expect(forNode.valueAlias!.content).toBe('item')
|
||||||
|
expect(forNode.valueAlias!.loc.start.offset).toBe(
|
||||||
|
source.indexOf('item') - 1
|
||||||
|
)
|
||||||
|
expect(forNode.valueAlias!.loc.start.line).toBe(1)
|
||||||
|
expect(forNode.valueAlias!.loc.start.column).toBe(source.indexOf('item'))
|
||||||
|
expect(forNode.valueAlias!.loc.end.line).toBe(1)
|
||||||
|
expect(forNode.valueAlias!.loc.end.column).toBe(
|
||||||
|
source.indexOf('item') + 4
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(forNode.objectIndexAlias!.content).toBe('index')
|
||||||
|
expect(forNode.objectIndexAlias!.loc.start.offset).toBe(
|
||||||
|
source.indexOf('index') - 1
|
||||||
|
)
|
||||||
|
expect(forNode.objectIndexAlias!.loc.start.line).toBe(1)
|
||||||
|
expect(forNode.objectIndexAlias!.loc.start.column).toBe(
|
||||||
|
source.indexOf('index')
|
||||||
|
)
|
||||||
|
expect(forNode.objectIndexAlias!.loc.end.line).toBe(1)
|
||||||
|
expect(forNode.objectIndexAlias!.loc.end.column).toBe(
|
||||||
|
source.indexOf('index') + 5
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(forNode.source.content).toBe('items')
|
||||||
|
expect(forNode.source.loc.start.offset).toBe(source.indexOf('items') - 1)
|
||||||
|
expect(forNode.source.loc.start.line).toBe(1)
|
||||||
|
expect(forNode.source.loc.start.column).toBe(source.indexOf('items'))
|
||||||
|
expect(forNode.source.loc.end.line).toBe(1)
|
||||||
|
expect(forNode.source.loc.end.column).toBe(source.indexOf('items') + 5)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
69
packages/compiler-core/__tests__/utils.spec.ts
Normal file
69
packages/compiler-core/__tests__/utils.spec.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { Position } from '../src/ast'
|
||||||
|
import { getInnerRange, advancePositionBy } from '../src/utils'
|
||||||
|
|
||||||
|
function p(line: number, column: number, offset: number): Position {
|
||||||
|
return { column, line, offset }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('advancePositionBy', () => {
|
||||||
|
test('same line', () => {
|
||||||
|
const pos = p(1, 1, 0)
|
||||||
|
const newPos = advancePositionBy(pos, 'foo\nbar', 2)
|
||||||
|
|
||||||
|
expect(newPos.column).toBe(3)
|
||||||
|
expect(newPos.line).toBe(1)
|
||||||
|
expect(newPos.offset).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('same line', () => {
|
||||||
|
const pos = p(1, 1, 0)
|
||||||
|
const newPos = advancePositionBy(pos, 'foo\nbar', 4)
|
||||||
|
|
||||||
|
expect(newPos.column).toBe(1)
|
||||||
|
expect(newPos.line).toBe(2)
|
||||||
|
expect(newPos.offset).toBe(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('multiple lines', () => {
|
||||||
|
const pos = p(1, 1, 0)
|
||||||
|
const newPos = advancePositionBy(pos, 'foo\nbar\nbaz', 10)
|
||||||
|
|
||||||
|
expect(newPos.column).toBe(2)
|
||||||
|
expect(newPos.line).toBe(3)
|
||||||
|
expect(newPos.offset).toBe(10)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getInnerRange', () => {
|
||||||
|
const loc1 = {
|
||||||
|
source: 'foo\nbar\nbaz',
|
||||||
|
start: p(1, 1, 0),
|
||||||
|
end: p(3, 3, 11)
|
||||||
|
}
|
||||||
|
|
||||||
|
test('at start', () => {
|
||||||
|
const loc2 = getInnerRange(loc1, 0, 4)
|
||||||
|
expect(loc2.start).toEqual(loc1.start)
|
||||||
|
expect(loc2.end.column).toBe(1)
|
||||||
|
expect(loc2.end.line).toBe(2)
|
||||||
|
expect(loc2.end.offset).toBe(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('at end', () => {
|
||||||
|
const loc2 = getInnerRange(loc1, 4)
|
||||||
|
expect(loc2.start.column).toBe(1)
|
||||||
|
expect(loc2.start.line).toBe(2)
|
||||||
|
expect(loc2.start.offset).toBe(4)
|
||||||
|
expect(loc2.end).toEqual(loc1.end)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('in between', () => {
|
||||||
|
const loc2 = getInnerRange(loc1, 4, 3)
|
||||||
|
expect(loc2.start.column).toBe(1)
|
||||||
|
expect(loc2.start.line).toBe(2)
|
||||||
|
expect(loc2.start.offset).toBe(4)
|
||||||
|
expect(loc2.end.column).toBe(3)
|
||||||
|
expect(loc2.end.line).toBe(2)
|
||||||
|
expect(loc2.end.offset).toBe(7)
|
||||||
|
})
|
||||||
|
})
|
@ -102,9 +102,9 @@ export interface IfBranchNode extends Node {
|
|||||||
export interface ForNode extends Node {
|
export interface ForNode extends Node {
|
||||||
type: NodeTypes.FOR
|
type: NodeTypes.FOR
|
||||||
source: ExpressionNode
|
source: ExpressionNode
|
||||||
valueAlias: ExpressionNode
|
valueAlias: ExpressionNode | undefined
|
||||||
keyAlias: ExpressionNode
|
keyAlias: ExpressionNode | undefined
|
||||||
objectIndexAlias: ExpressionNode
|
objectIndexAlias: ExpressionNode | undefined
|
||||||
children: ChildNode[]
|
children: ChildNode[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1 +1,137 @@
|
|||||||
// TODO
|
import { createDirectiveTransform, TransformContext } from '../transform'
|
||||||
|
import { NodeTypes, ExpressionNode, Node, SourceLocation } from '../ast'
|
||||||
|
import { createCompilerError, ErrorCodes } from '../errors'
|
||||||
|
import { getInnerRange } from '../utils'
|
||||||
|
|
||||||
|
const forAliasRE = /([\s\S]*?)(?:(?<=\))|\s+)(?:in|of)\s+([\s\S]*)/
|
||||||
|
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
|
||||||
|
const stripParensRE = /^\(|\)$/g
|
||||||
|
export const transformFor = createDirectiveTransform(
|
||||||
|
'for',
|
||||||
|
(node, dir, context) => {
|
||||||
|
if (dir.exp) {
|
||||||
|
const aliases = parseAliasExpressions(dir.exp.content)
|
||||||
|
|
||||||
|
if (aliases) {
|
||||||
|
context.replaceNode({
|
||||||
|
type: NodeTypes.FOR,
|
||||||
|
loc: node.loc,
|
||||||
|
source: createExpression(aliases.source, dir.exp, context),
|
||||||
|
valueAlias: aliases.value
|
||||||
|
? createExpression(aliases.value, dir.exp, context)
|
||||||
|
: undefined,
|
||||||
|
keyAlias: aliases.key
|
||||||
|
? createExpression(aliases.key, dir.exp, context)
|
||||||
|
: undefined,
|
||||||
|
objectIndexAlias: aliases.index
|
||||||
|
? createExpression(aliases.index, dir.exp, context)
|
||||||
|
: undefined,
|
||||||
|
children: [node]
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
context.onError(
|
||||||
|
createCompilerError(
|
||||||
|
ErrorCodes.X_FOR_MALFORMED_EXPRESSION,
|
||||||
|
dir.loc.start
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
context.onError(
|
||||||
|
createCompilerError(ErrorCodes.X_FOR_NO_EXPRESSION, dir.loc.start)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function createExpression(
|
||||||
|
alias: AliasExpression,
|
||||||
|
node: Node,
|
||||||
|
context: TransformContext
|
||||||
|
): ExpressionNode {
|
||||||
|
const loc: SourceLocation = getInnerRange(
|
||||||
|
node.loc,
|
||||||
|
alias.offset,
|
||||||
|
alias.content.length
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: NodeTypes.EXPRESSION,
|
||||||
|
loc: loc,
|
||||||
|
content: alias.content,
|
||||||
|
isStatic: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AliasExpression {
|
||||||
|
offset: number
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AliasExpressions {
|
||||||
|
source: AliasExpression
|
||||||
|
value: AliasExpression | undefined
|
||||||
|
key: AliasExpression | undefined
|
||||||
|
index: AliasExpression | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAliasExpressions(source: string): null | AliasExpressions {
|
||||||
|
const inMatch = source.match(forAliasRE)
|
||||||
|
|
||||||
|
if (!inMatch) return null
|
||||||
|
|
||||||
|
const [, LHS, RHS] = inMatch
|
||||||
|
|
||||||
|
const result: AliasExpressions = {
|
||||||
|
source: {
|
||||||
|
offset: source.indexOf(RHS, LHS.length),
|
||||||
|
content: RHS.trim()
|
||||||
|
},
|
||||||
|
value: undefined,
|
||||||
|
key: undefined,
|
||||||
|
index: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
let valueContent = LHS.trim()
|
||||||
|
.replace(stripParensRE, '')
|
||||||
|
.trim()
|
||||||
|
const trimmedOffset = LHS.indexOf(valueContent)
|
||||||
|
|
||||||
|
const iteratorMatch = valueContent.match(forIteratorRE)
|
||||||
|
if (iteratorMatch) {
|
||||||
|
valueContent = valueContent.replace(forIteratorRE, '').trim()
|
||||||
|
|
||||||
|
const keyContent = iteratorMatch[1].trim()
|
||||||
|
if (keyContent) {
|
||||||
|
result.key = {
|
||||||
|
offset: source.indexOf(keyContent, trimmedOffset + valueContent.length),
|
||||||
|
content: keyContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iteratorMatch[2]) {
|
||||||
|
const indexContent = iteratorMatch[2].trim()
|
||||||
|
|
||||||
|
if (indexContent) {
|
||||||
|
result.index = {
|
||||||
|
offset: source.indexOf(
|
||||||
|
indexContent,
|
||||||
|
result.key
|
||||||
|
? result.key.offset + result.key.content.length
|
||||||
|
: trimmedOffset + valueContent.length
|
||||||
|
),
|
||||||
|
content: indexContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueContent) {
|
||||||
|
result.value = {
|
||||||
|
offset: trimmedOffset,
|
||||||
|
content: valueContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
@ -59,7 +59,9 @@ export const enum ErrorCodes {
|
|||||||
|
|
||||||
// transform errors
|
// transform errors
|
||||||
X_ELSE_IF_NO_ADJACENT_IF,
|
X_ELSE_IF_NO_ADJACENT_IF,
|
||||||
X_ELSE_NO_ADJACENT_IF
|
X_ELSE_NO_ADJACENT_IF,
|
||||||
|
X_FOR_NO_EXPRESSION,
|
||||||
|
X_FOR_MALFORMED_EXPRESSION
|
||||||
}
|
}
|
||||||
|
|
||||||
export const errorMessages: { [code: number]: string } = {
|
export const errorMessages: { [code: number]: string } = {
|
||||||
@ -116,5 +118,7 @@ export const errorMessages: { [code: number]: string } = {
|
|||||||
|
|
||||||
// transform errors
|
// transform errors
|
||||||
[ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF]: `v-else-if has no adjacent v-if`,
|
[ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF]: `v-else-if has no adjacent v-if`,
|
||||||
[ErrorCodes.X_ELSE_NO_ADJACENT_IF]: `v-else has no adjacent v-if`
|
[ErrorCodes.X_ELSE_NO_ADJACENT_IF]: `v-else has no adjacent v-if`,
|
||||||
|
[ErrorCodes.X_FOR_NO_EXPRESSION]: `v-for has no expression`,
|
||||||
|
[ErrorCodes.X_FOR_MALFORMED_EXPRESSION]: `v-for has invalid expression`
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
TextNode,
|
TextNode,
|
||||||
ChildNode
|
ChildNode
|
||||||
} from './ast'
|
} from './ast'
|
||||||
|
import { assert, advancePositionBy } from './utils'
|
||||||
|
|
||||||
export interface ParserOptions {
|
export interface ParserOptions {
|
||||||
isVoidTag?: (tag: string) => boolean // e.g. img, br, hr
|
isVoidTag?: (tag: string) => boolean // e.g. img, br, hr
|
||||||
@ -795,17 +796,13 @@ function startsWith(source: string, searchString: string): boolean {
|
|||||||
function advanceBy(context: ParserContext, numberOfCharacters: number): void {
|
function advanceBy(context: ParserContext, numberOfCharacters: number): void {
|
||||||
__DEV__ && assert(numberOfCharacters <= context.source.length)
|
__DEV__ && assert(numberOfCharacters <= context.source.length)
|
||||||
|
|
||||||
const { column, source } = context
|
const { source } = context
|
||||||
const str = source.slice(0, numberOfCharacters)
|
const pos = advancePositionBy(context, source, numberOfCharacters)
|
||||||
const lines = str.split(/\r?\n/)
|
|
||||||
|
|
||||||
context.source = source.slice(numberOfCharacters)
|
context.source = source.slice(numberOfCharacters)
|
||||||
context.offset += numberOfCharacters
|
context.offset = pos.offset
|
||||||
context.line += lines.length - 1
|
context.line = pos.line
|
||||||
context.column =
|
context.column = pos.column
|
||||||
lines.length === 1
|
|
||||||
? column + numberOfCharacters
|
|
||||||
: Math.max(1, lines.pop()!.length)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function advanceSpaces(context: ParserContext): void {
|
function advanceSpaces(context: ParserContext): void {
|
||||||
@ -899,12 +896,6 @@ function startsWithEndTagOpen(source: string, tag: string): boolean {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function assert(condition: boolean, msg?: string) {
|
|
||||||
if (!condition) {
|
|
||||||
throw new Error(msg || `unexpected parser condition`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state
|
// https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state
|
||||||
const CCR_REPLACEMENTS: { [key: number]: number | undefined } = {
|
const CCR_REPLACEMENTS: { [key: number]: number | undefined } = {
|
||||||
0x80: 0x20ac,
|
0x80: 0x20ac,
|
||||||
|
50
packages/compiler-core/src/utils.ts
Normal file
50
packages/compiler-core/src/utils.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { SourceLocation, Position } from './ast'
|
||||||
|
|
||||||
|
export function getInnerRange(
|
||||||
|
loc: SourceLocation,
|
||||||
|
offset: number,
|
||||||
|
length?: number
|
||||||
|
): SourceLocation {
|
||||||
|
const source = loc.source.substr(offset, length)
|
||||||
|
const newLoc: SourceLocation = {
|
||||||
|
source,
|
||||||
|
start: advancePositionBy(loc.start, loc.source, offset),
|
||||||
|
end: loc.end
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length != null) {
|
||||||
|
newLoc.end = advancePositionBy(loc.start, loc.source, offset + length)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newLoc
|
||||||
|
}
|
||||||
|
|
||||||
|
export function advancePositionBy(
|
||||||
|
pos: Position,
|
||||||
|
source: string,
|
||||||
|
numberOfCharacters: number
|
||||||
|
): Position {
|
||||||
|
__DEV__ && assert(numberOfCharacters <= source.length)
|
||||||
|
|
||||||
|
const newPosition = {
|
||||||
|
...pos
|
||||||
|
}
|
||||||
|
|
||||||
|
const str = source.slice(0, numberOfCharacters)
|
||||||
|
const lines = str.split(/\r?\n/)
|
||||||
|
|
||||||
|
newPosition.offset += numberOfCharacters
|
||||||
|
newPosition.line += lines.length - 1
|
||||||
|
newPosition.column =
|
||||||
|
lines.length === 1
|
||||||
|
? pos.column + numberOfCharacters
|
||||||
|
: Math.max(1, lines.pop()!.length)
|
||||||
|
|
||||||
|
return newPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assert(condition: boolean, msg?: string) {
|
||||||
|
if (!condition) {
|
||||||
|
throw new Error(msg || `unexpected parser condition`)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user