]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler-core): add parser transform for v-for directive (#65)
authorRahul Kadyan <hi@znck.me>
Thu, 19 Sep 2019 17:23:49 +0000 (22:53 +0530)
committerEvan You <yyx990803@gmail.com>
Thu, 19 Sep 2019 17:23:49 +0000 (13:23 -0400)
* 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

packages/compiler-core/__tests__/directives/vFor.spec.ts [new file with mode: 0644]
packages/compiler-core/__tests__/utils.spec.ts [new file with mode: 0644]
packages/compiler-core/src/ast.ts
packages/compiler-core/src/directives/vFor.ts
packages/compiler-core/src/errors.ts
packages/compiler-core/src/parse.ts
packages/compiler-core/src/utils.ts [new file with mode: 0644]

diff --git a/packages/compiler-core/__tests__/directives/vFor.spec.ts b/packages/compiler-core/__tests__/directives/vFor.spec.ts
new file mode 100644 (file)
index 0000000..9a937d4
--- /dev/null
@@ -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)
+    })
+  })
+})
diff --git a/packages/compiler-core/__tests__/utils.spec.ts b/packages/compiler-core/__tests__/utils.spec.ts
new file mode 100644 (file)
index 0000000..dffa6be
--- /dev/null
@@ -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)
+  })
+})
index b743e86f276fbf2903a1aed4a3982c20b8a9dc33..1d14158f1a7cccb71c789e502be0c4f6edc029c4 100644 (file)
@@ -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[]
 }
 
index 70b786d12ed055a08b57f5cf47f717bf6a266301..201adfa55699b481f467e640d9ba27c71ec691e1 100644 (file)
@@ -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
+}
index 735f20c98bc118a1f8614de29c9ead3996fa8fd9..e6288ac6c25a65fc6164492da0554a04a89c9873 100644 (file)
@@ -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`
 }
index 838b7910295a844a0813f8e5bfcaf96a236af846..831e009df808673232697c4971cf9c97efc9ab70 100644 (file)
@@ -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,
diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts
new file mode 100644 (file)
index 0000000..8206b40
--- /dev/null
@@ -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`)
+  }
+}