From 10c1a2b33278364c1d53011989d6a6d16a498c5e Mon Sep 17 00:00:00 2001 From: Rahul Kadyan Date: Thu, 19 Sep 2019 22:53:49 +0530 Subject: [PATCH] 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 --- .../__tests__/directives/vFor.spec.ts | 480 ++++++++++++++++++ .../compiler-core/__tests__/utils.spec.ts | 69 +++ packages/compiler-core/src/ast.ts | 6 +- packages/compiler-core/src/directives/vFor.ts | 138 ++++- packages/compiler-core/src/errors.ts | 8 +- packages/compiler-core/src/parse.ts | 21 +- packages/compiler-core/src/utils.ts | 50 ++ 7 files changed, 751 insertions(+), 21 deletions(-) create mode 100644 packages/compiler-core/__tests__/directives/vFor.spec.ts create mode 100644 packages/compiler-core/__tests__/utils.spec.ts create mode 100644 packages/compiler-core/src/utils.ts diff --git a/packages/compiler-core/__tests__/directives/vFor.spec.ts b/packages/compiler-core/__tests__/directives/vFor.spec.ts new file mode 100644 index 00000000..9a937d45 --- /dev/null +++ b/packages/compiler-core/__tests__/directives/vFor.spec.ts @@ -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('') + + 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('') + + 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('') + + 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('') + + 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('') + + 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('') + + 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('') + + 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('') + + 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('') + + 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('') + + 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('') + + 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('') + + 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('') + + 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('') + 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('') + 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('') + 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('') + 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('') + 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 = '' + 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 = '' + 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 = '' + 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 = '' + 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 = '' + 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 index 00000000..dffa6be8 --- /dev/null +++ b/packages/compiler-core/__tests__/utils.spec.ts @@ -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) + }) +}) diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index b743e86f..1d14158f 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -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[] } diff --git a/packages/compiler-core/src/directives/vFor.ts b/packages/compiler-core/src/directives/vFor.ts index 70b786d1..201adfa5 100644 --- a/packages/compiler-core/src/directives/vFor.ts +++ b/packages/compiler-core/src/directives/vFor.ts @@ -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 +} diff --git a/packages/compiler-core/src/errors.ts b/packages/compiler-core/src/errors.ts index 735f20c9..e6288ac6 100644 --- a/packages/compiler-core/src/errors.ts +++ b/packages/compiler-core/src/errors.ts @@ -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` } diff --git a/packages/compiler-core/src/parse.ts b/packages/compiler-core/src/parse.ts index 838b7910..831e009d 100644 --- a/packages/compiler-core/src/parse.ts +++ b/packages/compiler-core/src/parse.ts @@ -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 index 00000000..8206b40a --- /dev/null +++ b/packages/compiler-core/src/utils.ts @@ -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`) + } +}