feat(compiler): make interpolation symbols configurable (@Component config) (#9367)

closes #9158
This commit is contained in:
Victor Berchet
2016-06-20 09:52:41 -07:00
committed by GitHub
parent 6fd52dfb38
commit 1b28cf71f5
27 changed files with 403 additions and 125 deletions

View File

@ -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.`);
}
});
}
}

View File

@ -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;
@ -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 {
@ -634,7 +641,8 @@ export class CompileTemplateMetadata {
styles: data['styles'],
styleUrls: data['styleUrls'],
animations: animations,
ngContentSelectors: data['ngContentSelectors']
ngContentSelectors: data['ngContentSelectors'],
interpolation: data['interpolation']
});
}
@ -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
};
}
}

View File

@ -104,7 +104,8 @@ export class DirectiveNormalizer {
styles: allResolvedStyles,
styleUrls: allStyleAbsUrls,
ngContentSelectors: visitor.ngContentSelectors,
animations: templateMeta.animations
animations: templateMeta.animations,
interpolation: templateMeta.interpolation
});
}
}

View File

@ -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) {
@ -26,25 +25,36 @@ 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);
@ -52,7 +62,8 @@ export class Parser {
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);
@ -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();
}
@ -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[] = [];
@ -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;
}
@ -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);
@ -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;

View File

@ -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 {
@ -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();
}
@ -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();
@ -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 (
@ -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;
}
}
@ -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;
@ -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 {

View File

@ -4,6 +4,7 @@ import {BaseException} from '../facade/exceptions';
import {NumberWrapper, RegExpWrapper, isPresent} from '../facade/lang';
import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from '../html_ast';
import {HtmlParseTreeResult, HtmlParser} from '../html_parser';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../interpolation_config';
import {ParseError, ParseSourceSpan} from '../parse_util';
import {expandNodes} from './expander';
@ -96,15 +97,19 @@ let _PLACEHOLDER_EXPANDED_REGEXP = /<ph(\s)+name=("(\w)+")><\/ph>/gi;
*/
export class I18nHtmlParser implements HtmlParser {
errors: ParseError[];
private _interpolationConfig: InterpolationConfig;
constructor(
private _htmlParser: HtmlParser, private _parser: Parser, private _messagesContent: string,
private _messages: {[key: string]: HtmlAst[]}, private _implicitTags: string[],
private _implicitAttrs: {[k: string]: string[]}) {}
parse(sourceContent: string, sourceUrl: string, parseExpansionForms: boolean = false):
parse(
sourceContent: string, sourceUrl: string, parseExpansionForms: boolean = false,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG):
HtmlParseTreeResult {
this.errors = [];
this._interpolationConfig = interpolationConfig;
let res = this._htmlParser.parse(sourceContent, sourceUrl, true);
@ -134,7 +139,7 @@ export class I18nHtmlParser implements HtmlParser {
}
private _mergeI18Part(part: Part): HtmlAst[] {
let message = part.createMessage(this._parser);
let message = part.createMessage(this._parser, this._interpolationConfig);
let messageId = id(message);
if (!StringMapWrapper.contains(this._messages, messageId)) {
throw new I18nError(
@ -240,8 +245,8 @@ export class I18nHtmlParser implements HtmlParser {
}
private _mergeTextInterpolation(t: HtmlElementAst, originalNode: HtmlTextAst): HtmlTextAst {
let split =
this._parser.splitInterpolation(originalNode.value, originalNode.sourceSpan.toString());
let split = this._parser.splitInterpolation(
originalNode.value, originalNode.sourceSpan.toString(), this._interpolationConfig);
let exps = isPresent(split) ? split.expressions : [];
let messageSubstring =
@ -277,9 +282,9 @@ export class I18nHtmlParser implements HtmlParser {
res.push(attr);
return;
}
message = messageFromAttribute(this._parser, attr);
message = messageFromAttribute(this._parser, this._interpolationConfig, attr);
} else {
message = messageFromI18nAttribute(this._parser, el, i18ns[0]);
message = messageFromI18nAttribute(this._parser, this._interpolationConfig, el, i18ns[0]);
}
let messageId = id(message);
@ -298,7 +303,8 @@ export class I18nHtmlParser implements HtmlParser {
}
private _replaceInterpolationInAttr(attr: HtmlAttrAst, msg: HtmlAst[]): string {
let split = this._parser.splitInterpolation(attr.value, attr.sourceSpan.toString());
let split = this._parser.splitInterpolation(
attr.value, attr.sourceSpan.toString(), this._interpolationConfig);
let exps = isPresent(split) ? split.expressions : [];
let first = msg[0];
@ -336,7 +342,7 @@ export class I18nHtmlParser implements HtmlParser {
private _convertIntoExpression(
name: string, expMap: Map<string, string>, sourceSpan: ParseSourceSpan) {
if (expMap.has(name)) {
return `{{${expMap.get(name)}}}`;
return `${this._interpolationConfig.start}${expMap.get(name)}${this._interpolationConfig.end}`;
} else {
throw new I18nError(sourceSpan, `Invalid interpolation name '${name}'`);
}

View File

@ -3,6 +3,7 @@ import {StringMapWrapper} from '../facade/collection';
import {isPresent} from '../facade/lang';
import {HtmlAst, HtmlElementAst} from '../html_ast';
import {HtmlParser} from '../html_parser';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../interpolation_config';
import {ParseError} from '../parse_util';
import {expandNodes} from './expander';
@ -92,14 +93,18 @@ export function removeDuplicates(messages: Message[]): Message[] {
export class MessageExtractor {
private _messages: Message[];
private _errors: ParseError[];
private _interpolationConfig: InterpolationConfig;
constructor(
private _htmlParser: HtmlParser, private _parser: Parser, private _implicitTags: string[],
private _implicitAttrs: {[k: string]: string[]}) {}
extract(template: string, sourceUrl: string): ExtractionResult {
extract(
template: string, sourceUrl: string,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ExtractionResult {
this._messages = [];
this._errors = [];
this._interpolationConfig = interpolationConfig;
let res = this._htmlParser.parse(template, sourceUrl, true);
if (res.errors.length > 0) {
@ -113,7 +118,7 @@ export class MessageExtractor {
private _extractMessagesFromPart(part: Part): void {
if (part.hasI18n) {
this._messages.push(part.createMessage(this._parser));
this._messages.push(part.createMessage(this._parser, this._interpolationConfig));
this._recurseToExtractMessagesFromAttributes(part.children);
} else {
this._recurse(part.children);
@ -148,7 +153,8 @@ export class MessageExtractor {
p.attrs.filter(attr => attr.name.startsWith(I18N_ATTR_PREFIX)).forEach(attr => {
try {
explicitAttrs.push(attr.name.substring(I18N_ATTR_PREFIX.length));
this._messages.push(messageFromI18nAttribute(this._parser, p, attr));
this._messages.push(
messageFromI18nAttribute(this._parser, this._interpolationConfig, p, attr));
} catch (e) {
if (e instanceof I18nError) {
this._errors.push(e);
@ -161,6 +167,8 @@ export class MessageExtractor {
p.attrs.filter(attr => !attr.name.startsWith(I18N_ATTR_PREFIX))
.filter(attr => explicitAttrs.indexOf(attr.name) == -1)
.filter(attr => transAttrs.indexOf(attr.name) > -1)
.forEach(attr => this._messages.push(messageFromAttribute(this._parser, attr)));
.forEach(
attr => this._messages.push(
messageFromAttribute(this._parser, this._interpolationConfig, attr)));
}
}

View File

@ -1,6 +1,7 @@
import {Parser} from '../expression_parser/parser';
import {StringWrapper, isBlank, isPresent} from '../facade/lang';
import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from '../html_ast';
import {InterpolationConfig} from '../interpolation_config';
import {ParseError, ParseSourceSpan} from '../parse_util';
import {Message} from './message';
@ -61,9 +62,10 @@ export class Part {
return this.children[0].sourceSpan;
}
createMessage(parser: Parser): Message {
createMessage(parser: Parser, interpolationConfig: InterpolationConfig): Message {
return new Message(
stringifyNodes(this.children, parser), meaning(this.i18n), description(this.i18n));
stringifyNodes(this.children, parser, interpolationConfig), meaning(this.i18n),
description(this.i18n));
}
}
@ -102,28 +104,31 @@ export function description(i18n: string): string {
* @internal
*/
export function messageFromI18nAttribute(
parser: Parser, p: HtmlElementAst, i18nAttr: HtmlAttrAst): Message {
parser: Parser, interpolationConfig: InterpolationConfig, p: HtmlElementAst,
i18nAttr: HtmlAttrAst): Message {
let expectedName = i18nAttr.name.substring(5);
let attr = p.attrs.find(a => a.name == expectedName);
if (attr) {
return messageFromAttribute(parser, attr, meaning(i18nAttr.value), description(i18nAttr.value));
return messageFromAttribute(
parser, interpolationConfig, attr, meaning(i18nAttr.value), description(i18nAttr.value));
}
throw new I18nError(p.sourceSpan, `Missing attribute '${expectedName}'.`);
}
export function messageFromAttribute(
parser: Parser, attr: HtmlAttrAst, meaning: string = null,
description: string = null): Message {
let value = removeInterpolation(attr.value, attr.sourceSpan, parser);
parser: Parser, interpolationConfig: InterpolationConfig, attr: HtmlAttrAst,
meaning: string = null, description: string = null): Message {
let value = removeInterpolation(attr.value, attr.sourceSpan, parser, interpolationConfig);
return new Message(value, meaning, description);
}
export function removeInterpolation(
value: string, source: ParseSourceSpan, parser: Parser): string {
value: string, source: ParseSourceSpan, parser: Parser,
interpolationConfig: InterpolationConfig): string {
try {
let parsed = parser.splitInterpolation(value, source.toString());
let parsed = parser.splitInterpolation(value, source.toString(), interpolationConfig);
let usedNames = new Map<string, number>();
if (isPresent(parsed)) {
let res = '';
@ -160,14 +165,15 @@ export function dedupePhName(usedNames: Map<string, number>, name: string): stri
}
}
export function stringifyNodes(nodes: HtmlAst[], parser: Parser): string {
let visitor = new _StringifyVisitor(parser);
export function stringifyNodes(
nodes: HtmlAst[], parser: Parser, interpolationConfig: InterpolationConfig): string {
let visitor = new _StringifyVisitor(parser, interpolationConfig);
return htmlVisitAll(visitor, nodes).join('');
}
class _StringifyVisitor implements HtmlAstVisitor {
private _index: number = 0;
constructor(private _parser: Parser) {}
constructor(private _parser: Parser, private _interpolationConfig: InterpolationConfig) {}
visitElement(ast: HtmlElementAst, context: any): any {
let name = this._index++;
@ -179,7 +185,8 @@ class _StringifyVisitor implements HtmlAstVisitor {
visitText(ast: HtmlTextAst, context: any): any {
let index = this._index++;
let noInterpolation = removeInterpolation(ast.value, ast.sourceSpan, this._parser);
let noInterpolation =
removeInterpolation(ast.value, ast.sourceSpan, this._parser, this._interpolationConfig);
if (noInterpolation != ast.value) {
return `<ph name="t${index}">${noInterpolation}</ph>`;
}

View File

@ -0,0 +1,9 @@
export interface InterpolationConfig {
start: string;
end: string;
}
export const DEFAULT_INTERPOLATION_CONFIG: InterpolationConfig = {
start: '{{',
end: '}}'
};

View File

@ -5,7 +5,7 @@ import {StringMapWrapper} from '../src/facade/collection';
import {BaseException} from '../src/facade/exceptions';
import {Type, isArray, isBlank, isPresent, isString, isStringMap, stringify} from '../src/facade/lang';
import {assertArrayOfStrings} from './assertions';
import {assertArrayOfStrings, assertInterpolationSymbols} from './assertions';
import * as cpl from './compile_metadata';
import {CompilerConfig} from './config';
import {hasLifecycleHook} from './directive_lifecycle_reflector';
@ -96,6 +96,7 @@ export class CompileMetadataResolver {
var cmpMeta = <ComponentMetadata>dirMeta;
var viewMeta = this._viewResolver.resolve(directiveType);
assertArrayOfStrings('styles', viewMeta.styles);
assertInterpolationSymbols('interpolation', viewMeta.interpolation);
var animations = isPresent(viewMeta.animations) ?
viewMeta.animations.map(e => this.getAnimationEntryMetadata(e)) :
null;
@ -106,7 +107,8 @@ export class CompileMetadataResolver {
templateUrl: viewMeta.templateUrl,
styles: viewMeta.styles,
styleUrls: viewMeta.styleUrls,
animations: animations
animations: animations,
interpolation: viewMeta.interpolation
});
changeDetectionStrategy = cmpMeta.changeDetection;
if (isPresent(dirMeta.viewProviders)) {

View File

@ -11,6 +11,7 @@ import {CompileDirectiveMetadata, CompilePipeMetadata, CompileMetadataWithType,}
import {HtmlParser} from './html_parser';
import {splitNsName, mergeNsAndName} from './html_tags';
import {ParseSourceSpan, ParseError, ParseLocation, ParseErrorLevel} from './parse_util';
import {InterpolationConfig} from './interpolation_config';
import {ElementAst, BoundElementPropertyAst, BoundEventAst, ReferenceAst, TemplateAst, TemplateAstVisitor, templateVisitAll, TextAst, BoundTextAst, EmbeddedTemplateAst, AttrAst, NgContentAst, PropertyBindingType, DirectiveAst, BoundDirectivePropertyAst, ProviderAst, ProviderAstType, VariableAst} from './template_ast';
import {CssSelector, SelectorMatcher} from './selector';
@ -151,12 +152,20 @@ class TemplateParseVisitor implements HtmlAstVisitor {
directivesIndex = new Map<CompileDirectiveMetadata, number>();
ngContentCount: number = 0;
pipesByName: Map<string, CompilePipeMetadata>;
private _interpolationConfig: InterpolationConfig;
constructor(
public providerViewContext: ProviderViewContext, directives: CompileDirectiveMetadata[],
pipes: CompilePipeMetadata[], private _exprParser: Parser,
private _schemaRegistry: ElementSchemaRegistry) {
this.selectorMatcher = new SelectorMatcher();
const tempMeta = providerViewContext.component.template;
if (isPresent(tempMeta) && isPresent(tempMeta.interpolation)) {
this._interpolationConfig = {
start: tempMeta.interpolation[0],
end: tempMeta.interpolation[1]
};
}
ListWrapper.forEachWithIndex(
directives, (directive: CompileDirectiveMetadata, index: number) => {
var selector = CssSelector.parse(directive.selector);
@ -176,7 +185,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
private _parseInterpolation(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
var sourceInfo = sourceSpan.start.toString();
try {
var ast = this._exprParser.parseInterpolation(value, sourceInfo);
var ast = this._exprParser.parseInterpolation(value, sourceInfo, this._interpolationConfig);
this._checkPipes(ast, sourceSpan);
if (isPresent(ast) &&
(<Interpolation>ast.ast).expressions.length > MAX_INTERPOLATION_VALUES) {
@ -193,7 +202,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
private _parseAction(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
var sourceInfo = sourceSpan.start.toString();
try {
var ast = this._exprParser.parseAction(value, sourceInfo);
var ast = this._exprParser.parseAction(value, sourceInfo, this._interpolationConfig);
this._checkPipes(ast, sourceSpan);
return ast;
} catch (e) {
@ -205,7 +214,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
private _parseBinding(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
var sourceInfo = sourceSpan.start.toString();
try {
var ast = this._exprParser.parseBinding(value, sourceInfo);
var ast = this._exprParser.parseBinding(value, sourceInfo, this._interpolationConfig);
this._checkPipes(ast, sourceSpan);
return ast;
} catch (e) {

View File

@ -51,7 +51,8 @@ export class ViewResolver {
encapsulation: compMeta.encapsulation,
styles: compMeta.styles,
styleUrls: compMeta.styleUrls,
animations: compMeta.animations
animations: compMeta.animations,
interpolation: compMeta.interpolation
});
}
} else {