refactor: move angular source to /packages rather than modules/@angular

This commit is contained in:
Jason Aden
2017-03-02 10:48:42 -08:00
parent 5ad5301a3e
commit 3e51a19983
1051 changed files with 18 additions and 18 deletions

View File

@ -0,0 +1,392 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export class ParserError {
public message: string;
constructor(
message: string, public input: string, public errLocation: string, public ctxLocation?: any) {
this.message = `Parser Error: ${message} ${errLocation} [${input}] in ${ctxLocation}`;
}
}
export class ParseSpan {
constructor(public start: number, public end: number) {}
}
export class AST {
constructor(public span: ParseSpan) {}
visit(visitor: AstVisitor, context: any = null): any { return null; }
toString(): string { return 'AST'; }
}
/**
* Represents a quoted expression of the form:
*
* quote = prefix `:` uninterpretedExpression
* prefix = identifier
* uninterpretedExpression = arbitrary string
*
* A quoted expression is meant to be pre-processed by an AST transformer that
* converts it into another AST that no longer contains quoted expressions.
* It is meant to allow third-party developers to extend Angular template
* expression language. The `uninterpretedExpression` part of the quote is
* therefore not interpreted by the Angular's own expression parser.
*/
export class Quote extends AST {
constructor(
span: ParseSpan, public prefix: string, public uninterpretedExpression: string,
public location: any) {
super(span);
}
visit(visitor: AstVisitor, context: any = null): any { return visitor.visitQuote(this, context); }
toString(): string { return 'Quote'; }
}
export class EmptyExpr extends AST {
visit(visitor: AstVisitor, context: any = null) {
// do nothing
}
}
export class ImplicitReceiver extends AST {
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitImplicitReceiver(this, context);
}
}
/**
* Multiple expressions separated by a semicolon.
*/
export class Chain extends AST {
constructor(span: ParseSpan, public expressions: any[]) { super(span); }
visit(visitor: AstVisitor, context: any = null): any { return visitor.visitChain(this, context); }
}
export class Conditional extends AST {
constructor(span: ParseSpan, public condition: AST, public trueExp: AST, public falseExp: AST) {
super(span);
}
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitConditional(this, context);
}
}
export class PropertyRead extends AST {
constructor(span: ParseSpan, public receiver: AST, public name: string) { super(span); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitPropertyRead(this, context);
}
}
export class PropertyWrite extends AST {
constructor(span: ParseSpan, public receiver: AST, public name: string, public value: AST) {
super(span);
}
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitPropertyWrite(this, context);
}
}
export class SafePropertyRead extends AST {
constructor(span: ParseSpan, public receiver: AST, public name: string) { super(span); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitSafePropertyRead(this, context);
}
}
export class KeyedRead extends AST {
constructor(span: ParseSpan, public obj: AST, public key: AST) { super(span); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitKeyedRead(this, context);
}
}
export class KeyedWrite extends AST {
constructor(span: ParseSpan, public obj: AST, public key: AST, public value: AST) { super(span); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitKeyedWrite(this, context);
}
}
export class BindingPipe extends AST {
constructor(span: ParseSpan, public exp: AST, public name: string, public args: any[]) {
super(span);
}
visit(visitor: AstVisitor, context: any = null): any { return visitor.visitPipe(this, context); }
}
export class LiteralPrimitive extends AST {
constructor(span: ParseSpan, public value: any) { super(span); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitLiteralPrimitive(this, context);
}
}
export class LiteralArray extends AST {
constructor(span: ParseSpan, public expressions: any[]) { super(span); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitLiteralArray(this, context);
}
}
export class LiteralMap extends AST {
constructor(span: ParseSpan, public keys: any[], public values: any[]) { super(span); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitLiteralMap(this, context);
}
}
export class Interpolation extends AST {
constructor(span: ParseSpan, public strings: any[], public expressions: any[]) { super(span); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitInterpolation(this, context);
}
}
export class Binary extends AST {
constructor(span: ParseSpan, public operation: string, public left: AST, public right: AST) {
super(span);
}
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitBinary(this, context);
}
}
export class PrefixNot extends AST {
constructor(span: ParseSpan, public expression: AST) { super(span); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitPrefixNot(this, context);
}
}
export class MethodCall extends AST {
constructor(span: ParseSpan, public receiver: AST, public name: string, public args: any[]) {
super(span);
}
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitMethodCall(this, context);
}
}
export class SafeMethodCall extends AST {
constructor(span: ParseSpan, public receiver: AST, public name: string, public args: any[]) {
super(span);
}
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitSafeMethodCall(this, context);
}
}
export class FunctionCall extends AST {
constructor(span: ParseSpan, public target: AST, public args: any[]) { super(span); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitFunctionCall(this, context);
}
}
export class ASTWithSource extends AST {
constructor(
public ast: AST, public source: string, public location: string,
public errors: ParserError[]) {
super(new ParseSpan(0, source == null ? 0 : source.length));
}
visit(visitor: AstVisitor, context: any = null): any { return this.ast.visit(visitor, context); }
toString(): string { return `${this.source} in ${this.location}`; }
}
export class TemplateBinding {
constructor(
public span: ParseSpan, public key: string, public keyIsVar: boolean, public name: string,
public expression: ASTWithSource) {}
}
export interface AstVisitor {
visitBinary(ast: Binary, context: any): any;
visitChain(ast: Chain, context: any): any;
visitConditional(ast: Conditional, context: any): any;
visitFunctionCall(ast: FunctionCall, context: any): any;
visitImplicitReceiver(ast: ImplicitReceiver, context: any): any;
visitInterpolation(ast: Interpolation, context: any): any;
visitKeyedRead(ast: KeyedRead, context: any): any;
visitKeyedWrite(ast: KeyedWrite, context: any): any;
visitLiteralArray(ast: LiteralArray, context: any): any;
visitLiteralMap(ast: LiteralMap, context: any): any;
visitLiteralPrimitive(ast: LiteralPrimitive, context: any): any;
visitMethodCall(ast: MethodCall, context: any): any;
visitPipe(ast: BindingPipe, context: any): any;
visitPrefixNot(ast: PrefixNot, context: any): any;
visitPropertyRead(ast: PropertyRead, context: any): any;
visitPropertyWrite(ast: PropertyWrite, context: any): any;
visitQuote(ast: Quote, context: any): any;
visitSafeMethodCall(ast: SafeMethodCall, context: any): any;
visitSafePropertyRead(ast: SafePropertyRead, context: any): any;
}
export class RecursiveAstVisitor implements AstVisitor {
visitBinary(ast: Binary, context: any): any {
ast.left.visit(this);
ast.right.visit(this);
return null;
}
visitChain(ast: Chain, context: any): any { return this.visitAll(ast.expressions, context); }
visitConditional(ast: Conditional, context: any): any {
ast.condition.visit(this);
ast.trueExp.visit(this);
ast.falseExp.visit(this);
return null;
}
visitPipe(ast: BindingPipe, context: any): any {
ast.exp.visit(this);
this.visitAll(ast.args, context);
return null;
}
visitFunctionCall(ast: FunctionCall, context: any): any {
ast.target.visit(this);
this.visitAll(ast.args, context);
return null;
}
visitImplicitReceiver(ast: ImplicitReceiver, context: any): any { return null; }
visitInterpolation(ast: Interpolation, context: any): any {
return this.visitAll(ast.expressions, context);
}
visitKeyedRead(ast: KeyedRead, context: any): any {
ast.obj.visit(this);
ast.key.visit(this);
return null;
}
visitKeyedWrite(ast: KeyedWrite, context: any): any {
ast.obj.visit(this);
ast.key.visit(this);
ast.value.visit(this);
return null;
}
visitLiteralArray(ast: LiteralArray, context: any): any {
return this.visitAll(ast.expressions, context);
}
visitLiteralMap(ast: LiteralMap, context: any): any { return this.visitAll(ast.values, context); }
visitLiteralPrimitive(ast: LiteralPrimitive, context: any): any { return null; }
visitMethodCall(ast: MethodCall, context: any): any {
ast.receiver.visit(this);
return this.visitAll(ast.args, context);
}
visitPrefixNot(ast: PrefixNot, context: any): any {
ast.expression.visit(this);
return null;
}
visitPropertyRead(ast: PropertyRead, context: any): any {
ast.receiver.visit(this);
return null;
}
visitPropertyWrite(ast: PropertyWrite, context: any): any {
ast.receiver.visit(this);
ast.value.visit(this);
return null;
}
visitSafePropertyRead(ast: SafePropertyRead, context: any): any {
ast.receiver.visit(this);
return null;
}
visitSafeMethodCall(ast: SafeMethodCall, context: any): any {
ast.receiver.visit(this);
return this.visitAll(ast.args, context);
}
visitAll(asts: AST[], context: any): any {
asts.forEach(ast => ast.visit(this, context));
return null;
}
visitQuote(ast: Quote, context: any): any { return null; }
}
export class AstTransformer implements AstVisitor {
visitImplicitReceiver(ast: ImplicitReceiver, context: any): AST { return ast; }
visitInterpolation(ast: Interpolation, context: any): AST {
return new Interpolation(ast.span, ast.strings, this.visitAll(ast.expressions));
}
visitLiteralPrimitive(ast: LiteralPrimitive, context: any): AST {
return new LiteralPrimitive(ast.span, ast.value);
}
visitPropertyRead(ast: PropertyRead, context: any): AST {
return new PropertyRead(ast.span, ast.receiver.visit(this), ast.name);
}
visitPropertyWrite(ast: PropertyWrite, context: any): AST {
return new PropertyWrite(ast.span, ast.receiver.visit(this), ast.name, ast.value.visit(this));
}
visitSafePropertyRead(ast: SafePropertyRead, context: any): AST {
return new SafePropertyRead(ast.span, ast.receiver.visit(this), ast.name);
}
visitMethodCall(ast: MethodCall, context: any): AST {
return new MethodCall(ast.span, ast.receiver.visit(this), ast.name, this.visitAll(ast.args));
}
visitSafeMethodCall(ast: SafeMethodCall, context: any): AST {
return new SafeMethodCall(
ast.span, ast.receiver.visit(this), ast.name, this.visitAll(ast.args));
}
visitFunctionCall(ast: FunctionCall, context: any): AST {
return new FunctionCall(ast.span, ast.target.visit(this), this.visitAll(ast.args));
}
visitLiteralArray(ast: LiteralArray, context: any): AST {
return new LiteralArray(ast.span, this.visitAll(ast.expressions));
}
visitLiteralMap(ast: LiteralMap, context: any): AST {
return new LiteralMap(ast.span, ast.keys, this.visitAll(ast.values));
}
visitBinary(ast: Binary, context: any): AST {
return new Binary(ast.span, ast.operation, ast.left.visit(this), ast.right.visit(this));
}
visitPrefixNot(ast: PrefixNot, context: any): AST {
return new PrefixNot(ast.span, ast.expression.visit(this));
}
visitConditional(ast: Conditional, context: any): AST {
return new Conditional(
ast.span, ast.condition.visit(this), ast.trueExp.visit(this), ast.falseExp.visit(this));
}
visitPipe(ast: BindingPipe, context: any): AST {
return new BindingPipe(ast.span, ast.exp.visit(this), ast.name, this.visitAll(ast.args));
}
visitKeyedRead(ast: KeyedRead, context: any): AST {
return new KeyedRead(ast.span, ast.obj.visit(this), ast.key.visit(this));
}
visitKeyedWrite(ast: KeyedWrite, context: any): AST {
return new KeyedWrite(
ast.span, ast.obj.visit(this), ast.key.visit(this), ast.value.visit(this));
}
visitAll(asts: any[]): any[] {
const res = new Array(asts.length);
for (let i = 0; i < asts.length; ++i) {
res[i] = asts[i].visit(this);
}
return res;
}
visitChain(ast: Chain, context: any): AST {
return new Chain(ast.span, this.visitAll(ast.expressions));
}
visitQuote(ast: Quote, context: any): AST {
return new Quote(ast.span, ast.prefix, ast.uninterpretedExpression, ast.location);
}
}

View File

@ -0,0 +1,392 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as chars from '../chars';
import {CompilerInjectable} from '../injectable';
export enum TokenType {
Character,
Identifier,
Keyword,
String,
Operator,
Number,
Error
}
const KEYWORDS = ['var', 'let', 'null', 'undefined', 'true', 'false', 'if', 'else', 'this'];
@CompilerInjectable()
export class Lexer {
tokenize(text: string): Token[] {
const scanner = new _Scanner(text);
const tokens: Token[] = [];
let token = scanner.scanToken();
while (token != null) {
tokens.push(token);
token = scanner.scanToken();
}
return tokens;
}
}
export class Token {
constructor(
public index: number, public type: TokenType, public numValue: number,
public strValue: string) {}
isCharacter(code: number): boolean {
return this.type == TokenType.Character && this.numValue == code;
}
isNumber(): boolean { return this.type == TokenType.Number; }
isString(): boolean { return this.type == TokenType.String; }
isOperator(operater: string): boolean {
return this.type == TokenType.Operator && this.strValue == operater;
}
isIdentifier(): boolean { return this.type == TokenType.Identifier; }
isKeyword(): boolean { return this.type == TokenType.Keyword; }
isKeywordLet(): boolean { return this.type == TokenType.Keyword && this.strValue == 'let'; }
isKeywordNull(): boolean { return this.type == TokenType.Keyword && this.strValue == 'null'; }
isKeywordUndefined(): boolean {
return this.type == TokenType.Keyword && this.strValue == 'undefined';
}
isKeywordTrue(): boolean { return this.type == TokenType.Keyword && this.strValue == 'true'; }
isKeywordFalse(): boolean { return this.type == TokenType.Keyword && this.strValue == 'false'; }
isKeywordThis(): boolean { return this.type == TokenType.Keyword && this.strValue == 'this'; }
isError(): boolean { return this.type == TokenType.Error; }
toNumber(): number { return this.type == TokenType.Number ? this.numValue : -1; }
toString(): string {
switch (this.type) {
case TokenType.Character:
case TokenType.Identifier:
case TokenType.Keyword:
case TokenType.Operator:
case TokenType.String:
case TokenType.Error:
return this.strValue;
case TokenType.Number:
return this.numValue.toString();
default:
return null;
}
}
}
function newCharacterToken(index: number, code: number): Token {
return new Token(index, TokenType.Character, code, String.fromCharCode(code));
}
function newIdentifierToken(index: number, text: string): Token {
return new Token(index, TokenType.Identifier, 0, text);
}
function newKeywordToken(index: number, text: string): Token {
return new Token(index, TokenType.Keyword, 0, text);
}
function newOperatorToken(index: number, text: string): Token {
return new Token(index, TokenType.Operator, 0, text);
}
function newStringToken(index: number, text: string): Token {
return new Token(index, TokenType.String, 0, text);
}
function newNumberToken(index: number, n: number): Token {
return new Token(index, TokenType.Number, n, '');
}
function newErrorToken(index: number, message: string): Token {
return new Token(index, TokenType.Error, 0, message);
}
export const EOF: Token = new Token(-1, TokenType.Character, 0, '');
class _Scanner {
length: number;
peek: number = 0;
index: number = -1;
constructor(public input: string) {
this.length = input.length;
this.advance();
}
advance() {
this.peek = ++this.index >= this.length ? chars.$EOF : this.input.charCodeAt(this.index);
}
scanToken(): Token {
const input = this.input, length = this.length;
let peek = this.peek, index = this.index;
// Skip whitespace.
while (peek <= chars.$SPACE) {
if (++index >= length) {
peek = chars.$EOF;
break;
} else {
peek = input.charCodeAt(index);
}
}
this.peek = peek;
this.index = index;
if (index >= length) {
return null;
}
// Handle identifiers and numbers.
if (isIdentifierStart(peek)) return this.scanIdentifier();
if (chars.isDigit(peek)) return this.scanNumber(index);
const start: number = index;
switch (peek) {
case chars.$PERIOD:
this.advance();
return chars.isDigit(this.peek) ? this.scanNumber(start) :
newCharacterToken(start, chars.$PERIOD);
case chars.$LPAREN:
case chars.$RPAREN:
case chars.$LBRACE:
case chars.$RBRACE:
case chars.$LBRACKET:
case chars.$RBRACKET:
case chars.$COMMA:
case chars.$COLON:
case chars.$SEMICOLON:
return this.scanCharacter(start, peek);
case chars.$SQ:
case chars.$DQ:
return this.scanString();
case chars.$HASH:
case chars.$PLUS:
case chars.$MINUS:
case chars.$STAR:
case chars.$SLASH:
case chars.$PERCENT:
case chars.$CARET:
return this.scanOperator(start, String.fromCharCode(peek));
case chars.$QUESTION:
return this.scanComplexOperator(start, '?', chars.$PERIOD, '.');
case chars.$LT:
case chars.$GT:
return this.scanComplexOperator(start, String.fromCharCode(peek), chars.$EQ, '=');
case chars.$BANG:
case chars.$EQ:
return this.scanComplexOperator(
start, String.fromCharCode(peek), chars.$EQ, '=', chars.$EQ, '=');
case chars.$AMPERSAND:
return this.scanComplexOperator(start, '&', chars.$AMPERSAND, '&');
case chars.$BAR:
return this.scanComplexOperator(start, '|', chars.$BAR, '|');
case chars.$NBSP:
while (chars.isWhitespace(this.peek)) this.advance();
return this.scanToken();
}
this.advance();
return this.error(`Unexpected character [${String.fromCharCode(peek)}]`, 0);
}
scanCharacter(start: number, code: number): Token {
this.advance();
return newCharacterToken(start, code);
}
scanOperator(start: number, str: string): Token {
this.advance();
return newOperatorToken(start, str);
}
/**
* Tokenize a 2/3 char long operator
*
* @param start start index in the expression
* @param one first symbol (always part of the operator)
* @param twoCode code point for the second symbol
* @param two second symbol (part of the operator when the second code point matches)
* @param threeCode code point for the third symbol
* @param three third symbol (part of the operator when provided and matches source expression)
* @returns {Token}
*/
scanComplexOperator(
start: number, one: string, twoCode: number, two: string, threeCode?: number,
three?: string): Token {
this.advance();
let str: string = one;
if (this.peek == twoCode) {
this.advance();
str += two;
}
if (threeCode != null && this.peek == threeCode) {
this.advance();
str += three;
}
return newOperatorToken(start, str);
}
scanIdentifier(): Token {
const start: number = this.index;
this.advance();
while (isIdentifierPart(this.peek)) this.advance();
const str: string = this.input.substring(start, this.index);
return KEYWORDS.indexOf(str) > -1 ? newKeywordToken(start, str) :
newIdentifierToken(start, str);
}
scanNumber(start: number): Token {
let simple: boolean = (this.index === start);
this.advance(); // Skip initial digit.
while (true) {
if (chars.isDigit(this.peek)) {
// Do nothing.
} else if (this.peek == chars.$PERIOD) {
simple = false;
} else if (isExponentStart(this.peek)) {
this.advance();
if (isExponentSign(this.peek)) this.advance();
if (!chars.isDigit(this.peek)) return this.error('Invalid exponent', -1);
simple = false;
} else {
break;
}
this.advance();
}
const str: string = this.input.substring(start, this.index);
const value: number = simple ? parseIntAutoRadix(str) : parseFloat(str);
return newNumberToken(start, value);
}
scanString(): Token {
const start: number = this.index;
const quote: number = this.peek;
this.advance(); // Skip initial quote.
let buffer: string = '';
let marker: number = this.index;
const input: string = this.input;
while (this.peek != quote) {
if (this.peek == chars.$BACKSLASH) {
buffer += input.substring(marker, this.index);
this.advance();
let unescapedCode: number;
// Workaround for TS2.1-introduced type strictness
this.peek = this.peek;
if (this.peek == chars.$u) {
// 4 character hex code for unicode character.
const hex: string = input.substring(this.index + 1, this.index + 5);
if (/^[0-9a-f]+$/i.test(hex)) {
unescapedCode = parseInt(hex, 16);
} else {
return this.error(`Invalid unicode escape [\\u${hex}]`, 0);
}
for (let i: number = 0; i < 5; i++) {
this.advance();
}
} else {
unescapedCode = unescape(this.peek);
this.advance();
}
buffer += String.fromCharCode(unescapedCode);
marker = this.index;
} else if (this.peek == chars.$EOF) {
return this.error('Unterminated quote', 0);
} else {
this.advance();
}
}
const last: string = input.substring(marker, this.index);
this.advance(); // Skip terminating quote.
return newStringToken(start, buffer + last);
}
error(message: string, offset: number): Token {
const position: number = this.index + offset;
return newErrorToken(
position, `Lexer Error: ${message} at column ${position} in expression [${this.input}]`);
}
}
function isIdentifierStart(code: number): boolean {
return (chars.$a <= code && code <= chars.$z) || (chars.$A <= code && code <= chars.$Z) ||
(code == chars.$_) || (code == chars.$$);
}
export function isIdentifier(input: string): boolean {
if (input.length == 0) return false;
const scanner = new _Scanner(input);
if (!isIdentifierStart(scanner.peek)) return false;
scanner.advance();
while (scanner.peek !== chars.$EOF) {
if (!isIdentifierPart(scanner.peek)) return false;
scanner.advance();
}
return true;
}
function isIdentifierPart(code: number): boolean {
return chars.isAsciiLetter(code) || chars.isDigit(code) || (code == chars.$_) ||
(code == chars.$$);
}
function isExponentStart(code: number): boolean {
return code == chars.$e || code == chars.$E;
}
function isExponentSign(code: number): boolean {
return code == chars.$MINUS || code == chars.$PLUS;
}
export function isQuote(code: number): boolean {
return code === chars.$SQ || code === chars.$DQ || code === chars.$BT;
}
function unescape(code: number): number {
switch (code) {
case chars.$n:
return chars.$LF;
case chars.$f:
return chars.$FF;
case chars.$r:
return chars.$CR;
case chars.$t:
return chars.$TAB;
case chars.$v:
return chars.$VTAB;
default:
return code;
}
}
function parseIntAutoRadix(text: string): number {
const result: number = parseInt(text);
if (isNaN(result)) {
throw new Error('Invalid integer literal when parsing ' + text);
}
return result;
}

View File

@ -0,0 +1,812 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as chars from '../chars';
import {CompilerInjectable} from '../injectable';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config';
import {escapeRegExp} from '../util';
import {AST, ASTWithSource, AstVisitor, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, ParseSpan, ParserError, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead, TemplateBinding} from './ast';
import {EOF, Lexer, Token, TokenType, isIdentifier, isQuote} from './lexer';
export class SplitInterpolation {
constructor(public strings: string[], public expressions: string[], public offsets: number[]) {}
}
export class TemplateBindingParseResult {
constructor(
public templateBindings: TemplateBinding[], public warnings: string[],
public errors: ParserError[]) {}
}
function _createInterpolateRegExp(config: InterpolationConfig): RegExp {
const pattern = escapeRegExp(config.start) + '([\\s\\S]*?)' + escapeRegExp(config.end);
return new RegExp(pattern, 'g');
}
@CompilerInjectable()
export class Parser {
private errors: ParserError[] = [];
constructor(private _lexer: Lexer) {}
parseAction(
input: string, location: any,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
this._checkNoInterpolation(input, location, interpolationConfig);
const sourceToLex = this._stripComments(input);
const tokens = this._lexer.tokenize(this._stripComments(input));
const ast = new _ParseAST(
input, location, tokens, sourceToLex.length, true, this.errors,
input.length - sourceToLex.length)
.parseChain();
return new ASTWithSource(ast, input, location, this.errors);
}
parseBinding(
input: string, location: any,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
const ast = this._parseBindingAst(input, location, interpolationConfig);
return new ASTWithSource(ast, input, location, this.errors);
}
parseSimpleBinding(
input: string, location: string,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
const ast = this._parseBindingAst(input, location, interpolationConfig);
const errors = SimpleExpressionChecker.check(ast);
if (errors.length > 0) {
this._reportError(
`Host binding expression cannot contain ${errors.join(' ')}`, input, location);
}
return new ASTWithSource(ast, input, location, this.errors);
}
private _reportError(message: string, input: string, errLocation: string, ctxLocation?: any) {
this.errors.push(new ParserError(message, input, errLocation, ctxLocation));
}
private _parseBindingAst(
input: string, location: string, interpolationConfig: InterpolationConfig): AST {
// Quotes expressions use 3rd-party expression language. We don't want to use
// our lexer or parser for that, so we check for that ahead of time.
const quote = this._parseQuote(input, location);
if (quote != null) {
return quote;
}
this._checkNoInterpolation(input, location, interpolationConfig);
const sourceToLex = this._stripComments(input);
const tokens = this._lexer.tokenize(sourceToLex);
return new _ParseAST(
input, location, tokens, sourceToLex.length, false, this.errors,
input.length - sourceToLex.length)
.parseChain();
}
private _parseQuote(input: string, location: any): AST {
if (input == null) return null;
const prefixSeparatorIndex = input.indexOf(':');
if (prefixSeparatorIndex == -1) return null;
const prefix = input.substring(0, prefixSeparatorIndex).trim();
if (!isIdentifier(prefix)) return null;
const uninterpretedExpression = input.substring(prefixSeparatorIndex + 1);
return new Quote(new ParseSpan(0, input.length), prefix, uninterpretedExpression, location);
}
parseTemplateBindings(prefixToken: string, input: string, location: any):
TemplateBindingParseResult {
const tokens = this._lexer.tokenize(input);
if (prefixToken) {
// Prefix the tokens with the tokens from prefixToken but have them take no space (0 index).
const prefixTokens = this._lexer.tokenize(prefixToken).map(t => {
t.index = 0;
return t;
});
tokens.unshift(...prefixTokens);
}
return new _ParseAST(input, location, tokens, input.length, false, this.errors, 0)
.parseTemplateBindings();
}
parseInterpolation(
input: string, location: any,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
const split = this.splitInterpolation(input, location, interpolationConfig);
if (split == null) return null;
const expressions: AST[] = [];
for (let i = 0; i < split.expressions.length; ++i) {
const expressionText = split.expressions[i];
const sourceToLex = this._stripComments(expressionText);
const tokens = this._lexer.tokenize(this._stripComments(split.expressions[i]));
const ast = new _ParseAST(
input, location, tokens, sourceToLex.length, false, this.errors,
split.offsets[i] + (expressionText.length - sourceToLex.length))
.parseChain();
expressions.push(ast);
}
return new ASTWithSource(
new Interpolation(
new ParseSpan(0, input == null ? 0 : input.length), split.strings, expressions),
input, location, this.errors);
}
splitInterpolation(
input: string, location: string,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): SplitInterpolation {
const regexp = _createInterpolateRegExp(interpolationConfig);
const parts = input.split(regexp);
if (parts.length <= 1) {
return null;
}
const strings: string[] = [];
const expressions: string[] = [];
const offsets: number[] = [];
let offset = 0;
for (let i = 0; i < parts.length; i++) {
const part: string = parts[i];
if (i % 2 === 0) {
// fixed string
strings.push(part);
offset += part.length;
} else if (part.trim().length > 0) {
offset += interpolationConfig.start.length;
expressions.push(part);
offsets.push(offset);
offset += part.length + interpolationConfig.end.length;
} else {
this._reportError(
'Blank expressions are not allowed in interpolated strings', input,
`at column ${this._findInterpolationErrorColumn(parts, i, interpolationConfig)} in`,
location);
expressions.push('$implict');
offsets.push(offset);
}
}
return new SplitInterpolation(strings, expressions, offsets);
}
wrapLiteralPrimitive(input: string, location: any): ASTWithSource {
return new ASTWithSource(
new LiteralPrimitive(new ParseSpan(0, input == null ? 0 : input.length), input), input,
location, this.errors);
}
private _stripComments(input: string): string {
const i = this._commentStart(input);
return i != null ? input.substring(0, i).trim() : input;
}
private _commentStart(input: string): number {
let outerQuote: number = null;
for (let i = 0; i < input.length - 1; i++) {
const char = input.charCodeAt(i);
const nextChar = input.charCodeAt(i + 1);
if (char === chars.$SLASH && nextChar == chars.$SLASH && outerQuote == null) return i;
if (outerQuote === char) {
outerQuote = null;
} else if (outerQuote == null && isQuote(char)) {
outerQuote = char;
}
}
return null;
}
private _checkNoInterpolation(
input: string, location: any, interpolationConfig: InterpolationConfig): void {
const regexp = _createInterpolateRegExp(interpolationConfig);
const parts = input.split(regexp);
if (parts.length > 1) {
this._reportError(
`Got interpolation (${interpolationConfig.start}${interpolationConfig.end}) where expression was expected`,
input,
`at column ${this._findInterpolationErrorColumn(parts, 1, interpolationConfig)} in`,
location);
}
}
private _findInterpolationErrorColumn(
parts: string[], partInErrIdx: number, interpolationConfig: InterpolationConfig): number {
let errLocation = '';
for (let j = 0; j < partInErrIdx; j++) {
errLocation += j % 2 === 0 ?
parts[j] :
`${interpolationConfig.start}${parts[j]}${interpolationConfig.end}`;
}
return errLocation.length;
}
}
export class _ParseAST {
private rparensExpected = 0;
private rbracketsExpected = 0;
private rbracesExpected = 0;
index: number = 0;
constructor(
public input: string, public location: any, public tokens: Token[],
public inputLength: number, public parseAction: boolean, private errors: ParserError[],
private offset: number) {}
peek(offset: number): Token {
const i = this.index + offset;
return i < this.tokens.length ? this.tokens[i] : EOF;
}
get next(): Token { return this.peek(0); }
get inputIndex(): number {
return (this.index < this.tokens.length) ? this.next.index + this.offset :
this.inputLength + this.offset;
}
span(start: number) { return new ParseSpan(start, this.inputIndex); }
advance() { this.index++; }
optionalCharacter(code: number): boolean {
if (this.next.isCharacter(code)) {
this.advance();
return true;
} else {
return false;
}
}
peekKeywordLet(): boolean { return this.next.isKeywordLet(); }
expectCharacter(code: number) {
if (this.optionalCharacter(code)) return;
this.error(`Missing expected ${String.fromCharCode(code)}`);
}
optionalOperator(op: string): boolean {
if (this.next.isOperator(op)) {
this.advance();
return true;
} else {
return false;
}
}
expectOperator(operator: string) {
if (this.optionalOperator(operator)) return;
this.error(`Missing expected operator ${operator}`);
}
expectIdentifierOrKeyword(): string {
const n = this.next;
if (!n.isIdentifier() && !n.isKeyword()) {
this.error(`Unexpected token ${n}, expected identifier or keyword`);
return '';
}
this.advance();
return n.toString();
}
expectIdentifierOrKeywordOrString(): string {
const n = this.next;
if (!n.isIdentifier() && !n.isKeyword() && !n.isString()) {
this.error(`Unexpected token ${n}, expected identifier, keyword, or string`);
return '';
}
this.advance();
return n.toString();
}
parseChain(): AST {
const exprs: AST[] = [];
const start = this.inputIndex;
while (this.index < this.tokens.length) {
const expr = this.parsePipe();
exprs.push(expr);
if (this.optionalCharacter(chars.$SEMICOLON)) {
if (!this.parseAction) {
this.error('Binding expression cannot contain chained expression');
}
while (this.optionalCharacter(chars.$SEMICOLON)) {
} // read all semicolons
} else if (this.index < this.tokens.length) {
this.error(`Unexpected token '${this.next}'`);
}
}
if (exprs.length == 0) return new EmptyExpr(this.span(start));
if (exprs.length == 1) return exprs[0];
return new Chain(this.span(start), exprs);
}
parsePipe(): AST {
let result = this.parseExpression();
if (this.optionalOperator('|')) {
if (this.parseAction) {
this.error('Cannot have a pipe in an action expression');
}
do {
const name = this.expectIdentifierOrKeyword();
const args: AST[] = [];
while (this.optionalCharacter(chars.$COLON)) {
args.push(this.parseExpression());
}
result = new BindingPipe(this.span(result.span.start), result, name, args);
} while (this.optionalOperator('|'));
}
return result;
}
parseExpression(): AST { return this.parseConditional(); }
parseConditional(): AST {
const start = this.inputIndex;
const result = this.parseLogicalOr();
if (this.optionalOperator('?')) {
const yes = this.parsePipe();
let no: AST;
if (!this.optionalCharacter(chars.$COLON)) {
const end = this.inputIndex;
const expression = this.input.substring(start, end);
this.error(`Conditional expression ${expression} requires all 3 expressions`);
no = new EmptyExpr(this.span(start));
} else {
no = this.parsePipe();
}
return new Conditional(this.span(start), result, yes, no);
} else {
return result;
}
}
parseLogicalOr(): AST {
// '||'
let result = this.parseLogicalAnd();
while (this.optionalOperator('||')) {
const right = this.parseLogicalAnd();
result = new Binary(this.span(result.span.start), '||', result, right);
}
return result;
}
parseLogicalAnd(): AST {
// '&&'
let result = this.parseEquality();
while (this.optionalOperator('&&')) {
const right = this.parseEquality();
result = new Binary(this.span(result.span.start), '&&', result, right);
}
return result;
}
parseEquality(): AST {
// '==','!=','===','!=='
let result = this.parseRelational();
while (this.next.type == TokenType.Operator) {
const operator = this.next.strValue;
switch (operator) {
case '==':
case '===':
case '!=':
case '!==':
this.advance();
const right = this.parseRelational();
result = new Binary(this.span(result.span.start), operator, result, right);
continue;
}
break;
}
return result;
}
parseRelational(): AST {
// '<', '>', '<=', '>='
let result = this.parseAdditive();
while (this.next.type == TokenType.Operator) {
const operator = this.next.strValue;
switch (operator) {
case '<':
case '>':
case '<=':
case '>=':
this.advance();
const right = this.parseAdditive();
result = new Binary(this.span(result.span.start), operator, result, right);
continue;
}
break;
}
return result;
}
parseAdditive(): AST {
// '+', '-'
let result = this.parseMultiplicative();
while (this.next.type == TokenType.Operator) {
const operator = this.next.strValue;
switch (operator) {
case '+':
case '-':
this.advance();
let right = this.parseMultiplicative();
result = new Binary(this.span(result.span.start), operator, result, right);
continue;
}
break;
}
return result;
}
parseMultiplicative(): AST {
// '*', '%', '/'
let result = this.parsePrefix();
while (this.next.type == TokenType.Operator) {
const operator = this.next.strValue;
switch (operator) {
case '*':
case '%':
case '/':
this.advance();
let right = this.parsePrefix();
result = new Binary(this.span(result.span.start), operator, result, right);
continue;
}
break;
}
return result;
}
parsePrefix(): AST {
if (this.next.type == TokenType.Operator) {
const start = this.inputIndex;
const operator = this.next.strValue;
let result: AST;
switch (operator) {
case '+':
this.advance();
return this.parsePrefix();
case '-':
this.advance();
result = this.parsePrefix();
return new Binary(
this.span(start), operator, new LiteralPrimitive(new ParseSpan(start, start), 0),
result);
case '!':
this.advance();
result = this.parsePrefix();
return new PrefixNot(this.span(start), result);
}
}
return this.parseCallChain();
}
parseCallChain(): AST {
let result = this.parsePrimary();
while (true) {
if (this.optionalCharacter(chars.$PERIOD)) {
result = this.parseAccessMemberOrMethodCall(result, false);
} else if (this.optionalOperator('?.')) {
result = this.parseAccessMemberOrMethodCall(result, true);
} else if (this.optionalCharacter(chars.$LBRACKET)) {
this.rbracketsExpected++;
const key = this.parsePipe();
this.rbracketsExpected--;
this.expectCharacter(chars.$RBRACKET);
if (this.optionalOperator('=')) {
const value = this.parseConditional();
result = new KeyedWrite(this.span(result.span.start), result, key, value);
} else {
result = new KeyedRead(this.span(result.span.start), result, key);
}
} else if (this.optionalCharacter(chars.$LPAREN)) {
this.rparensExpected++;
const args = this.parseCallArguments();
this.rparensExpected--;
this.expectCharacter(chars.$RPAREN);
result = new FunctionCall(this.span(result.span.start), result, args);
} else {
return result;
}
}
}
parsePrimary(): AST {
const start = this.inputIndex;
if (this.optionalCharacter(chars.$LPAREN)) {
this.rparensExpected++;
const result = this.parsePipe();
this.rparensExpected--;
this.expectCharacter(chars.$RPAREN);
return result;
} else if (this.next.isKeywordNull()) {
this.advance();
return new LiteralPrimitive(this.span(start), null);
} else if (this.next.isKeywordUndefined()) {
this.advance();
return new LiteralPrimitive(this.span(start), void 0);
} else if (this.next.isKeywordTrue()) {
this.advance();
return new LiteralPrimitive(this.span(start), true);
} else if (this.next.isKeywordFalse()) {
this.advance();
return new LiteralPrimitive(this.span(start), false);
} else if (this.next.isKeywordThis()) {
this.advance();
return new ImplicitReceiver(this.span(start));
} else if (this.optionalCharacter(chars.$LBRACKET)) {
this.rbracketsExpected++;
const elements = this.parseExpressionList(chars.$RBRACKET);
this.rbracketsExpected--;
this.expectCharacter(chars.$RBRACKET);
return new LiteralArray(this.span(start), elements);
} else if (this.next.isCharacter(chars.$LBRACE)) {
return this.parseLiteralMap();
} else if (this.next.isIdentifier()) {
return this.parseAccessMemberOrMethodCall(new ImplicitReceiver(this.span(start)), false);
} else if (this.next.isNumber()) {
const value = this.next.toNumber();
this.advance();
return new LiteralPrimitive(this.span(start), value);
} else if (this.next.isString()) {
const literalValue = this.next.toString();
this.advance();
return new LiteralPrimitive(this.span(start), literalValue);
} else if (this.index >= this.tokens.length) {
this.error(`Unexpected end of expression: ${this.input}`);
return new EmptyExpr(this.span(start));
} else {
this.error(`Unexpected token ${this.next}`);
return new EmptyExpr(this.span(start));
}
}
parseExpressionList(terminator: number): AST[] {
const result: AST[] = [];
if (!this.next.isCharacter(terminator)) {
do {
result.push(this.parsePipe());
} while (this.optionalCharacter(chars.$COMMA));
}
return result;
}
parseLiteralMap(): LiteralMap {
const keys: string[] = [];
const values: AST[] = [];
const start = this.inputIndex;
this.expectCharacter(chars.$LBRACE);
if (!this.optionalCharacter(chars.$RBRACE)) {
this.rbracesExpected++;
do {
const key = this.expectIdentifierOrKeywordOrString();
keys.push(key);
this.expectCharacter(chars.$COLON);
values.push(this.parsePipe());
} while (this.optionalCharacter(chars.$COMMA));
this.rbracesExpected--;
this.expectCharacter(chars.$RBRACE);
}
return new LiteralMap(this.span(start), keys, values);
}
parseAccessMemberOrMethodCall(receiver: AST, isSafe: boolean = false): AST {
const start = receiver.span.start;
const id = this.expectIdentifierOrKeyword();
if (this.optionalCharacter(chars.$LPAREN)) {
this.rparensExpected++;
const args = this.parseCallArguments();
this.expectCharacter(chars.$RPAREN);
this.rparensExpected--;
const span = this.span(start);
return isSafe ? new SafeMethodCall(span, receiver, id, args) :
new MethodCall(span, receiver, id, args);
} else {
if (isSafe) {
if (this.optionalOperator('=')) {
this.error('The \'?.\' operator cannot be used in the assignment');
return new EmptyExpr(this.span(start));
} else {
return new SafePropertyRead(this.span(start), receiver, id);
}
} else {
if (this.optionalOperator('=')) {
if (!this.parseAction) {
this.error('Bindings cannot contain assignments');
return new EmptyExpr(this.span(start));
}
const value = this.parseConditional();
return new PropertyWrite(this.span(start), receiver, id, value);
} else {
return new PropertyRead(this.span(start), receiver, id);
}
}
}
}
parseCallArguments(): BindingPipe[] {
if (this.next.isCharacter(chars.$RPAREN)) return [];
const positionals: AST[] = [];
do {
positionals.push(this.parsePipe());
} while (this.optionalCharacter(chars.$COMMA));
return positionals as BindingPipe[];
}
/**
* An identifier, a keyword, a string with an optional `-` inbetween.
*/
expectTemplateBindingKey(): string {
let result = '';
let operatorFound = false;
do {
result += this.expectIdentifierOrKeywordOrString();
operatorFound = this.optionalOperator('-');
if (operatorFound) {
result += '-';
}
} while (operatorFound);
return result.toString();
}
parseTemplateBindings(): TemplateBindingParseResult {
const bindings: TemplateBinding[] = [];
let prefix: string = null;
const warnings: string[] = [];
while (this.index < this.tokens.length) {
const start = this.inputIndex;
const keyIsVar: boolean = this.peekKeywordLet();
if (keyIsVar) {
this.advance();
}
let key = this.expectTemplateBindingKey();
if (!keyIsVar) {
if (prefix == null) {
prefix = key;
} else {
key = prefix + key[0].toUpperCase() + key.substring(1);
}
}
this.optionalCharacter(chars.$COLON);
let name: string = null;
let expression: ASTWithSource = null;
if (keyIsVar) {
if (this.optionalOperator('=')) {
name = this.expectTemplateBindingKey();
} else {
name = '\$implicit';
}
} else if (this.next !== EOF && !this.peekKeywordLet()) {
const start = this.inputIndex;
const ast = this.parsePipe();
const source = this.input.substring(start - this.offset, this.inputIndex - this.offset);
expression = new ASTWithSource(ast, source, this.location, this.errors);
}
bindings.push(new TemplateBinding(this.span(start), key, keyIsVar, name, expression));
if (!this.optionalCharacter(chars.$SEMICOLON)) {
this.optionalCharacter(chars.$COMMA);
}
}
return new TemplateBindingParseResult(bindings, warnings, this.errors);
}
error(message: string, index: number = null) {
this.errors.push(new ParserError(message, this.input, this.locationText(index), this.location));
this.skip();
}
private locationText(index: number = null) {
if (index == null) index = this.index;
return (index < this.tokens.length) ? `at column ${this.tokens[index].index + 1} in` :
`at the end of the expression`;
}
// Error recovery should skip tokens until it encounters a recovery point. skip() treats
// the end of input and a ';' as unconditionally a recovery point. It also treats ')',
// '}' and ']' as conditional recovery points if one of calling productions is expecting
// one of these symbols. This allows skip() to recover from errors such as '(a.) + 1' allowing
// more of the AST to be retained (it doesn't skip any tokens as the ')' is retained because
// of the '(' begins an '(' <expr> ')' production). The recovery points of grouping symbols
// must be conditional as they must be skipped if none of the calling productions are not
// expecting the closing token else we will never make progress in the case of an
// extraneous group closing symbol (such as a stray ')'). This is not the case for ';' because
// parseChain() is always the root production and it expects a ';'.
// If a production expects one of these token it increments the corresponding nesting count,
// and then decrements it just prior to checking if the token is in the input.
private skip() {
let n = this.next;
while (this.index < this.tokens.length && !n.isCharacter(chars.$SEMICOLON) &&
(this.rparensExpected <= 0 || !n.isCharacter(chars.$RPAREN)) &&
(this.rbracesExpected <= 0 || !n.isCharacter(chars.$RBRACE)) &&
(this.rbracketsExpected <= 0 || !n.isCharacter(chars.$RBRACKET))) {
if (this.next.isError()) {
this.errors.push(
new ParserError(this.next.toString(), this.input, this.locationText(), this.location));
}
this.advance();
n = this.next;
}
}
}
class SimpleExpressionChecker implements AstVisitor {
static check(ast: AST): string[] {
const s = new SimpleExpressionChecker();
ast.visit(s);
return s.errors;
}
errors: string[] = [];
visitImplicitReceiver(ast: ImplicitReceiver, context: any) {}
visitInterpolation(ast: Interpolation, context: any) {}
visitLiteralPrimitive(ast: LiteralPrimitive, context: any) {}
visitPropertyRead(ast: PropertyRead, context: any) {}
visitPropertyWrite(ast: PropertyWrite, context: any) {}
visitSafePropertyRead(ast: SafePropertyRead, context: any) {}
visitMethodCall(ast: MethodCall, context: any) {}
visitSafeMethodCall(ast: SafeMethodCall, context: any) {}
visitFunctionCall(ast: FunctionCall, context: any) {}
visitLiteralArray(ast: LiteralArray, context: any) { this.visitAll(ast.expressions); }
visitLiteralMap(ast: LiteralMap, context: any) { this.visitAll(ast.values); }
visitBinary(ast: Binary, context: any) {}
visitPrefixNot(ast: PrefixNot, context: any) {}
visitConditional(ast: Conditional, context: any) {}
visitPipe(ast: BindingPipe, context: any) { this.errors.push('pipes'); }
visitKeyedRead(ast: KeyedRead, context: any) {}
visitKeyedWrite(ast: KeyedWrite, context: any) {}
visitAll(asts: any[]): any[] { return asts.map(node => node.visit(this)); }
visitChain(ast: Chain, context: any) {}
visitQuote(ast: Quote, context: any) {}
}