Skip to content

Commit

Permalink
feat(compiler): make interpolation symbols configurable (@Component
Browse files Browse the repository at this point in the history
… config) (#9367)

closes #9158
  • Loading branch information
vicb committed Jun 20, 2016
1 parent 6fd52df commit 1b28cf7
Show file tree
Hide file tree
Showing 27 changed files with 403 additions and 125 deletions.
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

0 comments on commit 1b28cf7

Please sign in to comment.