Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(compiler): Introduce InterpolationConfig into component #9367

Merged
merged 1 commit into from Jun 20, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 21 additions & 0 deletions modules/@angular/compiler/src/assertions.ts
Expand Up @@ -16,3 +16,24 @@ export function assertArrayOfStrings(identifier: string, value: any) {
}
}
}

const INTERPOLATION_BLACKLIST_REGEXPS = [
/^\s*$/g, // empty
/[<>]/g, // html tag
/^[\{\}]$/g, // i18n expansion
];

export function assertInterpolationSymbols(identifier: string, value: any): void {
if (isDevMode() && !isBlank(value) && (!isArray(value) || value.length != 2)) {
throw new BaseException(`Expected '${identifier}' to be an array, [start, end].`);
} else if (isDevMode() && !isBlank(value)) {
const start = value[0] as string;
const end = value[1] as string;
// black list checking
INTERPOLATION_BLACKLIST_REGEXPS.forEach(regexp => {
if (regexp.test(start) || regexp.test(end)) {
throw new BaseException(`['${start}', '${end}'] contains unusable interpolation symbol.`);
}
});
}
}
17 changes: 13 additions & 4 deletions modules/@angular/compiler/src/compile_metadata.ts
Expand Up @@ -603,15 +603,18 @@ export class CompileTemplateMetadata {
styleUrls: string[];
animations: CompileAnimationEntryMetadata[];
ngContentSelectors: string[];
interpolation: [string, string];
constructor(
{encapsulation, template, templateUrl, styles, styleUrls, animations, ngContentSelectors}: {
{encapsulation, template, templateUrl, styles, styleUrls, animations, ngContentSelectors,
interpolation}: {
encapsulation?: ViewEncapsulation,
template?: string,
templateUrl?: string,
styles?: string[],
styleUrls?: string[],
ngContentSelectors?: string[],
animations?: CompileAnimationEntryMetadata[]
animations?: CompileAnimationEntryMetadata[],
interpolation?: [string, string]
} = {}) {
this.encapsulation = encapsulation;
this.template = template;
Expand All @@ -620,6 +623,10 @@ export class CompileTemplateMetadata {
this.styleUrls = isPresent(styleUrls) ? styleUrls : [];
this.animations = isPresent(animations) ? ListWrapper.flatten(animations) : [];
this.ngContentSelectors = isPresent(ngContentSelectors) ? ngContentSelectors : [];
if (isPresent(interpolation) && interpolation.length != 2) {
throw new BaseException(`'interpolation' should have a start and an end symbol.`);
}
this.interpolation = interpolation;
}

static fromJson(data: {[key: string]: any}): CompileTemplateMetadata {
Expand All @@ -634,7 +641,8 @@ export class CompileTemplateMetadata {
styles: data['styles'],
styleUrls: data['styleUrls'],
animations: animations,
ngContentSelectors: data['ngContentSelectors']
ngContentSelectors: data['ngContentSelectors'],
interpolation: data['interpolation']
});
}

Expand All @@ -647,7 +655,8 @@ export class CompileTemplateMetadata {
'styles': this.styles,
'styleUrls': this.styleUrls,
'animations': _objToJson(this.animations),
'ngContentSelectors': this.ngContentSelectors
'ngContentSelectors': this.ngContentSelectors,
'interpolation': this.interpolation
};
}
}
Expand Down
3 changes: 2 additions & 1 deletion modules/@angular/compiler/src/directive_normalizer.ts
Expand Up @@ -104,7 +104,8 @@ export class DirectiveNormalizer {
styles: allResolvedStyles,
styleUrls: allStyleAbsUrls,
ngContentSelectors: visitor.ngContentSelectors,
animations: templateMeta.animations
animations: templateMeta.animations,
interpolation: templateMeta.interpolation
});
}
}
Expand Down
68 changes: 46 additions & 22 deletions modules/@angular/compiler/src/expression_parser/parser.ts
Expand Up @@ -2,15 +2,14 @@ import {Injectable} from '@angular/core';

import {ListWrapper} from '../facade/collection';
import {BaseException} from '../facade/exceptions';
import {StringWrapper, isBlank, isPresent} from '../facade/lang';
import {RegExpWrapper, StringWrapper, escapeRegExp, isBlank, isPresent} from '../facade/lang';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../interpolation_config';

import {AST, ASTWithSource, AstVisitor, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead, TemplateBinding} from './ast';
import {$COLON, $COMMA, $LBRACE, $LBRACKET, $LPAREN, $PERIOD, $RBRACE, $RBRACKET, $RPAREN, $SEMICOLON, $SLASH, EOF, Lexer, Token, isIdentifier, isQuote} from './lexer';


var _implicitReceiver = new ImplicitReceiver();
// TODO(tbosch): Cannot make this const/final right now because of the transpiler...
var INTERPOLATION_REGEXP = /\{\{([\s\S]*?)\}\}/g;

class ParseException extends BaseException {
constructor(message: string, input: string, errLocation: string, ctxLocation?: any) {
Expand All @@ -26,33 +25,45 @@ export class TemplateBindingParseResult {
constructor(public templateBindings: TemplateBinding[], public warnings: string[]) {}
}

function _createInterpolateRegExp(config: InterpolationConfig): RegExp {
const regexp = escapeRegExp(config.start) + '([\\s\\S]*?)' + escapeRegExp(config.end);
return RegExpWrapper.create(regexp, 'g');
}

@Injectable()
export class Parser {
constructor(/** @internal */
public _lexer: Lexer) {}

parseAction(input: string, location: any): ASTWithSource {
this._checkNoInterpolation(input, location);
parseAction(
input: string, location: any,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
this._checkNoInterpolation(input, location, interpolationConfig);
var tokens = this._lexer.tokenize(this._stripComments(input));
var ast = new _ParseAST(input, location, tokens, true).parseChain();
return new ASTWithSource(ast, input, location);
}

parseBinding(input: string, location: any): ASTWithSource {
var ast = this._parseBindingAst(input, location);
parseBinding(
input: string, location: any,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
var ast = this._parseBindingAst(input, location, interpolationConfig);
return new ASTWithSource(ast, input, location);
}

parseSimpleBinding(input: string, location: string): ASTWithSource {
var ast = this._parseBindingAst(input, location);
parseSimpleBinding(
input: string, location: string,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
var ast = this._parseBindingAst(input, location, interpolationConfig);
if (!SimpleExpressionChecker.check(ast)) {
throw new ParseException(
'Host binding expression can only contain field access and constants', input, location);
}
return new ASTWithSource(ast, input, location);
}

private _parseBindingAst(input: string, location: string): AST {
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.
var quote = this._parseQuote(input, location);
Expand All @@ -61,7 +72,7 @@ export class Parser {
return quote;
}

this._checkNoInterpolation(input, location);
this._checkNoInterpolation(input, location, interpolationConfig);
var tokens = this._lexer.tokenize(this._stripComments(input));
return new _ParseAST(input, location, tokens, false).parseChain();
}
Expand All @@ -81,8 +92,10 @@ export class Parser {
return new _ParseAST(input, location, tokens, false).parseTemplateBindings();
}

parseInterpolation(input: string, location: any): ASTWithSource {
let split = this.splitInterpolation(input, location);
parseInterpolation(
input: string, location: any,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
let split = this.splitInterpolation(input, location, interpolationConfig);
if (split == null) return null;

let expressions: AST[] = [];
Expand All @@ -96,8 +109,11 @@ export class Parser {
return new ASTWithSource(new Interpolation(split.strings, expressions), input, location);
}

splitInterpolation(input: string, location: string): SplitInterpolation {
var parts = StringWrapper.split(input, INTERPOLATION_REGEXP);
splitInterpolation(
input: string, location: string,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): SplitInterpolation {
const regexp = _createInterpolateRegExp(interpolationConfig);
const parts = StringWrapper.split(input, regexp);
if (parts.length <= 1) {
return null;
}
Expand All @@ -114,7 +130,8 @@ export class Parser {
} else {
throw new ParseException(
'Blank expressions are not allowed in interpolated strings', input,
`at column ${this._findInterpolationErrorColumn(parts, i)} in`, location);
`at column ${this._findInterpolationErrorColumn(parts, i, interpolationConfig)} in`,
location);
}
}
return new SplitInterpolation(strings, expressions);
Expand Down Expand Up @@ -146,19 +163,26 @@ export class Parser {
return null;
}

private _checkNoInterpolation(input: string, location: any): void {
var parts = StringWrapper.split(input, INTERPOLATION_REGEXP);
private _checkNoInterpolation(
input: string, location: any, interpolationConfig: InterpolationConfig): void {
var regexp = _createInterpolateRegExp(interpolationConfig);
var parts = StringWrapper.split(input, regexp);
if (parts.length > 1) {
throw new ParseException(
'Got interpolation ({{}}) where expression was expected', input,
`at column ${this._findInterpolationErrorColumn(parts, 1)} in`, location);
`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): number {
private _findInterpolationErrorColumn(
parts: string[], partInErrIdx: number, interpolationConfig: InterpolationConfig): number {
var errLocation = '';
for (var j = 0; j < partInErrIdx; j++) {
errLocation += j % 2 === 0 ? parts[j] : `{{${parts[j]}}}`;
errLocation += j % 2 === 0 ?
parts[j] :
`${interpolationConfig.start}${parts[j]}${interpolationConfig.end}`;
}

return errLocation.length;
Expand Down
70 changes: 39 additions & 31 deletions modules/@angular/compiler/src/html_lexer.ts
Expand Up @@ -2,6 +2,7 @@ import * as chars from './chars';
import {ListWrapper} from './facade/collection';
import {NumberWrapper, StringWrapper, isBlank, isPresent} from './facade/lang';
import {HtmlTagContentType, NAMED_ENTITIES, getHtmlTagDefinition} from './html_tags';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './interpolation_config';
import {ParseError, ParseLocation, ParseSourceFile, ParseSourceSpan} from './parse_util';

export enum HtmlTokenType {
Expand Down Expand Up @@ -43,9 +44,11 @@ export class HtmlTokenizeResult {
}

export function tokenizeHtml(
sourceContent: string, sourceUrl: string,
tokenizeExpansionForms: boolean = false): HtmlTokenizeResult {
return new _HtmlTokenizer(new ParseSourceFile(sourceContent, sourceUrl), tokenizeExpansionForms)
sourceContent: string, sourceUrl: string, tokenizeExpansionForms: boolean = false,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): HtmlTokenizeResult {
return new _HtmlTokenizer(
new ParseSourceFile(sourceContent, sourceUrl), tokenizeExpansionForms,
interpolationConfig)
.tokenize();
}

Expand Down Expand Up @@ -81,7 +84,9 @@ class _HtmlTokenizer {
tokens: HtmlToken[] = [];
errors: HtmlTokenError[] = [];

constructor(private file: ParseSourceFile, private tokenizeExpansionForms: boolean) {
constructor(
private file: ParseSourceFile, private tokenizeExpansionForms: boolean,
private interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG) {
this._input = file.content;
this._length = file.content.length;
this._advance();
Expand Down Expand Up @@ -114,7 +119,8 @@ class _HtmlTokenizer {
this._consumeTagOpen(start);
}
} else if (
isExpansionFormStart(this._peek, this._nextPeek) && this.tokenizeExpansionForms) {
isExpansionFormStart(this._input, this._index, this.interpolationConfig.start) &&
this.tokenizeExpansionForms) {
this._consumeExpansionFormStart();

} else if (
Expand Down Expand Up @@ -232,16 +238,12 @@ class _HtmlTokenizer {
}

private _attemptStr(chars: string): boolean {
var indexBeforeAttempt = this._index;
var columnBeforeAttempt = this._column;
var lineBeforeAttempt = this._line;
const initialPosition = this._savePosition();
for (var i = 0; i < chars.length; i++) {
if (!this._attemptCharCode(StringWrapper.charCodeAt(chars, i))) {
// If attempting to parse the string fails, we want to reset the parser
// to where it was before the attempt
this._index = indexBeforeAttempt;
this._column = columnBeforeAttempt;
this._line = lineBeforeAttempt;
this._restorePosition(initialPosition);
return false;
}
}
Expand Down Expand Up @@ -558,35 +560,38 @@ class _HtmlTokenizer {
var parts: string[] = [];
let interpolation = false;

if (this._peek === chars.$LBRACE && this._nextPeek === chars.$LBRACE) {
parts.push(this._readChar(true));
parts.push(this._readChar(true));
interpolation = true;
} else {
parts.push(this._readChar(true));
}

while (!this._isTextEnd(interpolation)) {
if (this._peek === chars.$LBRACE && this._nextPeek === chars.$LBRACE) {
parts.push(this._readChar(true));
parts.push(this._readChar(true));
do {
const savedPos = this._savePosition();
// _attemptStr advances the position when it is true.
// To push interpolation symbols, we have to reset it.
if (this._attemptStr(this.interpolationConfig.start)) {
this._restorePosition(savedPos);
for (let i = 0; i < this.interpolationConfig.start.length; i++) {
parts.push(this._readChar(true));
}
interpolation = true;
} else if (
this._peek === chars.$RBRACE && this._nextPeek === chars.$RBRACE && interpolation) {
parts.push(this._readChar(true));
parts.push(this._readChar(true));
} else if (this._attemptStr(this.interpolationConfig.end) && interpolation) {
this._restorePosition(savedPos);
for (let i = 0; i < this.interpolationConfig.end.length; i++) {
parts.push(this._readChar(true));
}
interpolation = false;
} else {
this._restorePosition(savedPos);
parts.push(this._readChar(true));
}
}
} while (!this._isTextEnd(interpolation));

this._endToken([this._processCarriageReturns(parts.join(''))]);
}

private _isTextEnd(interpolation: boolean): boolean {
if (this._peek === chars.$LT || this._peek === chars.$EOF) return true;
if (this.tokenizeExpansionForms) {
if (isExpansionFormStart(this._peek, this._nextPeek)) return true;
const savedPos = this._savePosition();
if (isExpansionFormStart(this._input, this._index, this.interpolationConfig.start))
return true;
this._restorePosition(savedPos);
if (this._peek === chars.$RBRACE && !interpolation &&
(this._isInExpansionCase() || this._isInExpansionForm()))
return true;
Expand Down Expand Up @@ -655,8 +660,11 @@ function isNamedEntityEnd(code: number): boolean {
return code == chars.$SEMICOLON || code == chars.$EOF || !isAsciiLetter(code);
}

function isExpansionFormStart(peek: number, nextPeek: number): boolean {
return peek === chars.$LBRACE && nextPeek != chars.$LBRACE;
function isExpansionFormStart(input: string, offset: number, interpolationStart: string): boolean {
const substr = input.substring(offset);
return StringWrapper.charCodeAt(substr, 0) === chars.$LBRACE &&
StringWrapper.charCodeAt(substr, 1) !== chars.$LBRACE &&
!substr.startsWith(interpolationStart);
}

function isExpansionCaseStart(peek: number): boolean {
Expand Down