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 {
|
||||
type: NodeTypes.FOR
|
||||
source: ExpressionNode
|
||||
valueAlias: ExpressionNode
|
||||
keyAlias: ExpressionNode
|
||||
objectIndexAlias: ExpressionNode
|
||||
valueAlias: ExpressionNode | undefined
|
||||
keyAlias: ExpressionNode | undefined
|
||||
objectIndexAlias: ExpressionNode | undefined
|
||||
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
|
||||
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 } = {
|
||||
@ -116,5 +118,7 @@ export const errorMessages: { [code: number]: string } = {
|
||||
|
||||
// transform errors
|
||||
[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,
|
||||
ChildNode
|
||||
} from './ast'
|
||||
import { assert, advancePositionBy } from './utils'
|
||||
|
||||
export interface ParserOptions {
|
||||
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 {
|
||||
__DEV__ && assert(numberOfCharacters <= context.source.length)
|
||||
|
||||
const { column, source } = context
|
||||
const str = source.slice(0, numberOfCharacters)
|
||||
const lines = str.split(/\r?\n/)
|
||||
const { source } = context
|
||||
const pos = advancePositionBy(context, source, numberOfCharacters)
|
||||
|
||||
context.source = source.slice(numberOfCharacters)
|
||||
context.offset += numberOfCharacters
|
||||
context.line += lines.length - 1
|
||||
context.column =
|
||||
lines.length === 1
|
||||
? column + numberOfCharacters
|
||||
: Math.max(1, lines.pop()!.length)
|
||||
context.offset = pos.offset
|
||||
context.line = pos.line
|
||||
context.column = pos.column
|
||||
}
|
||||
|
||||
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
|
||||
const CCR_REPLACEMENTS: { [key: number]: number | undefined } = {
|
||||
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…
Reference in New Issue
Block a user