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 {

View File

@ -49,7 +49,8 @@ export function main() {
new CompileAnimationAnimateMetadata(
1000, new CompileAnimationStyleMetadata(0, [{'opacity': 1}]))
]))])],
ngContentSelectors: ['*']
ngContentSelectors: ['*'],
interpolation: ['{{', '}}']
});
fullDirectiveMeta = CompileDirectiveMetadata.create({
selector: 'someSelector',
@ -145,6 +146,11 @@ export function main() {
var empty = new CompileTemplateMetadata();
expect(CompileTemplateMetadata.fromJson(empty.toJson())).toEqual(empty);
});
it('should throw an error with invalid interpolation symbols', () => {
expect(() => new CompileTemplateMetadata(<any>{interpolation: ['{{']}))
.toThrowError(`'interpolation' should have a start and an end symbol.`);
});
});
describe('CompileAnimationStyleMetadata', () => {

View File

@ -467,6 +467,14 @@ export function main() {
checkInterpolation(`{{ 'foo' +\n 'bar' +\r 'baz' }}`, `{{ "foo" + "bar" + "baz" }}`);
});
it('should support custom interpolation', () => {
const parser = new Parser(new Lexer());
const ast = parser.parseInterpolation('{% a %}', null, {start: '{%', end: '%}'}).ast as any;
expect(ast.strings).toEqual(['', '']);
expect(ast.expressions.length).toEqual(1);
expect(ast.expressions[0].name).toEqual('a');
});
describe('comments', () => {
it('should ignore comments in interpolation expressions',
() => { checkInterpolation('{{a //comment}}', '{{ a }}'); });

View File

@ -1,12 +1,15 @@
import {AST, AstVisitor, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead} from '../../src/expression_parser/ast';
import {StringWrapper, isPresent, isString} from '../../src/facade/lang';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../src/interpolation_config';
export class Unparser implements AstVisitor {
private static _quoteRegExp = /"/g;
private _expression: string;
private _interpolationConfig: InterpolationConfig;
unparse(ast: AST) {
unparse(ast: AST, interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG) {
this._expression = '';
this._interpolationConfig = interpolationConfig;
this._visit(ast);
return this._expression;
}
@ -74,9 +77,9 @@ export class Unparser implements AstVisitor {
for (let i = 0; i < ast.strings.length; i++) {
this._expression += ast.strings[i];
if (i < ast.expressions.length) {
this._expression += '{{ ';
this._expression += `${this._interpolationConfig.start} `;
this._visit(ast.expressions[i]);
this._expression += ' }}';
this._expression += ` ${this._interpolationConfig.end}`;
}
}
}

View File

@ -1,4 +1,5 @@
import {HtmlToken, HtmlTokenError, HtmlTokenType, tokenizeHtml} from '@angular/compiler/src/html_lexer';
import {InterpolationConfig} from '@angular/compiler/src/interpolation_config';
import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '@angular/compiler/src/parse_util';
import {afterEach, beforeEach, ddescribe, describe, expect, iit, it, xit} from '@angular/core/testing/testing_internal';
@ -345,6 +346,16 @@ export function main() {
]);
});
it('should parse interpolation', () => {
expect(tokenizeAndHumanizeParts('{{ a }}')).toEqual([
[HtmlTokenType.TEXT, '{{ a }}'], [HtmlTokenType.EOF]
]);
expect(tokenizeAndHumanizeParts('{% a %}', null, {start: '{%', end: '%}'})).toEqual([
[HtmlTokenType.TEXT, '{% a %}'], [HtmlTokenType.EOF]
]);
});
it('should handle CR & LF', () => {
expect(tokenizeAndHumanizeParts('t\ne\rs\r\nt')).toEqual([
[HtmlTokenType.TEXT, 't\ne\ns\nt'], [HtmlTokenType.EOF]
@ -577,8 +588,9 @@ export function main() {
}
function tokenizeWithoutErrors(
input: string, tokenizeExpansionForms: boolean = false): HtmlToken[] {
var tokenizeResult = tokenizeHtml(input, 'someUrl', tokenizeExpansionForms);
input: string, tokenizeExpansionForms: boolean = false,
interpolationConfig?: InterpolationConfig): HtmlToken[] {
var tokenizeResult = tokenizeHtml(input, 'someUrl', tokenizeExpansionForms, interpolationConfig);
if (tokenizeResult.errors.length > 0) {
var errorString = tokenizeResult.errors.join('\n');
throw new BaseException(`Unexpected parse errors:\n${errorString}`);
@ -586,8 +598,10 @@ function tokenizeWithoutErrors(
return tokenizeResult.tokens;
}
function tokenizeAndHumanizeParts(input: string, tokenizeExpansionForms: boolean = false): any[] {
return tokenizeWithoutErrors(input, tokenizeExpansionForms)
function tokenizeAndHumanizeParts(
input: string, tokenizeExpansionForms: boolean = false,
interpolationConfig?: InterpolationConfig): any[] {
return tokenizeWithoutErrors(input, tokenizeExpansionForms, interpolationConfig)
.map(token => [<any>token.type].concat(token.parts));
}

View File

@ -5,6 +5,7 @@ import {HtmlParseTreeResult, HtmlParser} from '@angular/compiler/src/html_parser
import {I18nHtmlParser} from '@angular/compiler/src/i18n/i18n_html_parser';
import {Message, id} from '@angular/compiler/src/i18n/message';
import {deserializeXmb} from '@angular/compiler/src/i18n/xmb_serializer';
import {InterpolationConfig} from '@angular/compiler/src/interpolation_config';
import {ParseError} from '@angular/compiler/src/parse_util';
import {humanizeDom} from '@angular/compiler/test/html_ast_spec_utils';
import {ddescribe, describe, expect, iit, it} from '@angular/core/testing/testing_internal';
@ -15,7 +16,8 @@ export function main() {
describe('I18nHtmlParser', () => {
function parse(
template: string, messages: {[key: string]: string}, implicitTags: string[] = [],
implicitAttrs: {[k: string]: string[]} = {}): HtmlParseTreeResult {
implicitAttrs: {[k: string]: string[]} = {},
interpolation?: InterpolationConfig): HtmlParseTreeResult {
var parser = new Parser(new Lexer());
let htmlParser = new HtmlParser();
@ -26,7 +28,7 @@ export function main() {
return new I18nHtmlParser(
htmlParser, parser, res.content, res.messages, implicitTags, implicitAttrs)
.parse(template, 'someurl', true);
.parse(template, 'someurl', true, interpolation);
}
it('should delegate to the provided parser when no i18n', () => {
@ -63,6 +65,17 @@ export function main() {
.toEqual([[HtmlElementAst, 'div', 0], [HtmlAttrAst, 'value', '{{b}} or {{a}}']]);
});
it('should handle interpolation with config', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('<ph name="0"/> and <ph name="1"/>', null, null))] =
'<ph name="1"/> or <ph name="0"/>';
expect(humanizeDom(parse(
'<div value=\'{%a%} and {%b%}\' i18n-value></div>', translations, [], {},
{start: '{%', end: '%}'})))
.toEqual([[HtmlElementAst, 'div', 0], [HtmlAttrAst, 'value', '{%b%} or {%a%}']]);
});
it('should handle interpolation with custom placeholder names', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('<ph name="FIRST"/> and <ph name="SECOND"/>', null, null))] =

View File

@ -35,6 +35,7 @@ export function main() {
expect(meta.template.styleUrls).toEqual(['someStyleUrl']);
expect(meta.template.template).toEqual('someTemplate');
expect(meta.template.templateUrl).toEqual('someTemplateUrl');
expect(meta.template.interpolation).toEqual(['{{', '}}']);
}));
it('should use the moduleUrl from the reflector if none is given',
@ -61,6 +62,16 @@ export function main() {
.toThrowError(`Can't resolve all parameters for MyBrokenComp1: (?).`);
}
}));
it('should throw an error when the interpolation config has invalid symbols',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
expect(() => resolver.getDirectiveMetadata(ComponentWithInvalidInterpolation1))
.toThrowError(`[' ', ' '] contains unusable interpolation symbol.`);
expect(() => resolver.getDirectiveMetadata(ComponentWithInvalidInterpolation2))
.toThrowError(`['{', '}'] contains unusable interpolation symbol.`);
expect(() => resolver.getDirectiveMetadata(ComponentWithInvalidInterpolation3))
.toThrowError(`['<%', '%>'] contains unusable interpolation symbol.`);
}));
});
describe('getViewDirectivesMetadata', () => {
@ -120,7 +131,8 @@ class ComponentWithoutModuleId {
encapsulation: ViewEncapsulation.Emulated,
styles: ['someStyle'],
styleUrls: ['someStyleUrl'],
directives: [SomeDirective]
directives: [SomeDirective],
interpolation: ['{{', '}}']
})
class ComponentWithEverything implements OnChanges,
OnInit, DoCheck, OnDestroy, AfterContentInit, AfterContentChecked, AfterViewInit,
@ -139,3 +151,15 @@ class ComponentWithEverything implements OnChanges,
class MyBrokenComp1 {
constructor(public dependency: any) {}
}
@Component({selector: 'someSelector', template: '', interpolation: [' ', ' ']})
class ComponentWithInvalidInterpolation1 {
}
@Component({selector: 'someSelector', template: '', interpolation: ['{', '}']})
class ComponentWithInvalidInterpolation2 {
}
@Component({selector: 'someSelector', template: '', interpolation: ['<%', '%>']})
class ComponentWithInvalidInterpolation3 {
}

View File

@ -9,6 +9,7 @@ import {afterEach, beforeEach, beforeEachProviders, ddescribe, describe, expect,
import {SecurityContext} from '../core_private';
import {Identifiers, identifierToken} from '../src/identifiers';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../src/interpolation_config';
import {Unparser} from './expression_parser/unparser';
import {TEST_PROVIDERS} from './test_bindings';
@ -152,6 +153,20 @@ export function main() {
expect(humanizeTplAst(parse('{{a}}', []))).toEqual([[BoundTextAst, '{{ a }}']]);
});
it('should parse with custom interpolation config',
inject([TemplateParser], (parser: TemplateParser) => {
const component = CompileDirectiveMetadata.create({
selector: 'test',
type: new CompileTypeMetadata({moduleUrl: someModuleUrl, name: 'Test'}),
isComponent: true,
template: new CompileTemplateMetadata({interpolation: ['{%', '%}']})
});
expect(humanizeTplAst(parser.parse(component, '{%a%}', [], [], 'TestComp'), {
start: '{%',
end: '%}'
})).toEqual([[BoundTextAst, '{% a %}']]);
}));
describe('bound properties', () => {
it('should parse mixed case bound properties', () => {
@ -1406,14 +1421,16 @@ The pipe 'test' could not be found ("[ERROR ->]{{a | test}}"): TestComp@0:0`);
});
}
function humanizeTplAst(templateAsts: TemplateAst[]): any[] {
var humanizer = new TemplateHumanizer(false);
function humanizeTplAst(
templateAsts: TemplateAst[], interpolationConfig?: InterpolationConfig): any[] {
const humanizer = new TemplateHumanizer(false, interpolationConfig);
templateVisitAll(humanizer, templateAsts);
return humanizer.result;
}
function humanizeTplAstSourceSpans(templateAsts: TemplateAst[]): any[] {
var humanizer = new TemplateHumanizer(true);
function humanizeTplAstSourceSpans(
templateAsts: TemplateAst[], interpolationConfig?: InterpolationConfig): any[] {
const humanizer = new TemplateHumanizer(true, interpolationConfig);
templateVisitAll(humanizer, templateAsts);
return humanizer.result;
}
@ -1421,7 +1438,9 @@ function humanizeTplAstSourceSpans(templateAsts: TemplateAst[]): any[] {
class TemplateHumanizer implements TemplateAstVisitor {
result: any[] = [];
constructor(private includeSourceSpan: boolean){};
constructor(
private includeSourceSpan: boolean,
private interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG){};
visitNgContent(ast: NgContentAst, context: any): any {
var res = [NgContentAst];
@ -1461,13 +1480,17 @@ class TemplateHumanizer implements TemplateAstVisitor {
return null;
}
visitEvent(ast: BoundEventAst, context: any): any {
var res = [BoundEventAst, ast.name, ast.target, expressionUnparser.unparse(ast.handler)];
var res = [
BoundEventAst, ast.name, ast.target,
expressionUnparser.unparse(ast.handler, this.interpolationConfig)
];
this.result.push(this._appendContext(ast, res));
return null;
}
visitElementProperty(ast: BoundElementPropertyAst, context: any): any {
var res = [
BoundElementPropertyAst, ast.type, ast.name, expressionUnparser.unparse(ast.value), ast.unit
BoundElementPropertyAst, ast.type, ast.name,
expressionUnparser.unparse(ast.value, this.interpolationConfig), ast.unit
];
this.result.push(this._appendContext(ast, res));
return null;
@ -1478,7 +1501,7 @@ class TemplateHumanizer implements TemplateAstVisitor {
return null;
}
visitBoundText(ast: BoundTextAst, context: any): any {
var res = [BoundTextAst, expressionUnparser.unparse(ast.value)];
var res = [BoundTextAst, expressionUnparser.unparse(ast.value, this.interpolationConfig)];
this.result.push(this._appendContext(ast, res));
return null;
}
@ -1496,7 +1519,10 @@ class TemplateHumanizer implements TemplateAstVisitor {
return null;
}
visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any {
var res = [BoundDirectivePropertyAst, ast.directiveName, expressionUnparser.unparse(ast.value)];
var res = [
BoundDirectivePropertyAst, ast.directiveName,
expressionUnparser.unparse(ast.value, this.interpolationConfig)
];
this.result.push(this._appendContext(ast, res));
return null;
}

View File

@ -111,7 +111,8 @@ export class MockViewResolver extends ViewResolver {
styles: view.styles,
styleUrls: view.styleUrls,
pipes: view.pipes,
encapsulation: view.encapsulation
encapsulation: view.encapsulation,
interpolation: view.interpolation
});
this._viewCache.set(component, view);

View File

@ -42,7 +42,8 @@ export interface ComponentDecorator extends TypeDecorator {
renderer?: string,
styles?: string[],
styleUrls?: string[],
animations?: AnimationEntryMetadata[]
animations?: AnimationEntryMetadata[],
interpolation?: [string, string]
}): ViewDecorator;
}
@ -63,7 +64,8 @@ export interface ViewDecorator extends TypeDecorator {
renderer?: string,
styles?: string[],
styleUrls?: string[],
animations?: AnimationEntryMetadata[]
animations?: AnimationEntryMetadata[],
interpolation?: [string, string]
}): ViewDecorator;
}
@ -175,7 +177,8 @@ export interface ComponentMetadataFactory {
animations?: AnimationEntryMetadata[],
directives?: Array<Type|any[]>,
pipes?: Array<Type|any[]>,
encapsulation?: ViewEncapsulation
encapsulation?: ViewEncapsulation,
interpolation?: [string, string]
}): ComponentDecorator;
new (obj: {
selector?: string,
@ -197,7 +200,8 @@ export interface ComponentMetadataFactory {
animations?: AnimationEntryMetadata[],
directives?: Array<Type|any[]>,
pipes?: Array<Type|any[]>,
encapsulation?: ViewEncapsulation
encapsulation?: ViewEncapsulation,
interpolation?: [string, string]
}): ComponentMetadata;
}
@ -252,7 +256,8 @@ export interface ViewMetadataFactory {
encapsulation?: ViewEncapsulation,
styles?: string[],
styleUrls?: string[],
animations?: AnimationEntryMetadata[]
animations?: AnimationEntryMetadata[],
interpolation?: [string, string]
}): ViewDecorator;
new (obj: {
templateUrl?: string,
@ -262,7 +267,8 @@ export interface ViewMetadataFactory {
encapsulation?: ViewEncapsulation,
styles?: string[],
styleUrls?: string[],
animations?: AnimationEntryMetadata[]
animations?: AnimationEntryMetadata[],
interpolation?: [string, string]
}): ViewMetadata;
}

View File

@ -954,6 +954,8 @@ export class ComponentMetadata extends DirectiveMetadata {
encapsulation: ViewEncapsulation;
interpolation: [string, string];
constructor({selector,
inputs,
outputs,
@ -973,7 +975,8 @@ export class ComponentMetadata extends DirectiveMetadata {
animations,
directives,
pipes,
encapsulation}: {
encapsulation,
interpolation}: {
selector?: string,
inputs?: string[],
outputs?: string[],
@ -993,7 +996,8 @@ export class ComponentMetadata extends DirectiveMetadata {
animations?: AnimationEntryMetadata[],
directives?: Array<Type|any[]>,
pipes?: Array<Type|any[]>,
encapsulation?: ViewEncapsulation
encapsulation?: ViewEncapsulation,
interpolation?: [string, string]
} = {}) {
super({
selector: selector,
@ -1018,6 +1022,7 @@ export class ComponentMetadata extends DirectiveMetadata {
this.encapsulation = encapsulation;
this.moduleId = moduleId;
this.animations = animations;
this.interpolation = interpolation;
}
}

View File

@ -128,8 +128,11 @@ export class ViewMetadata {
animations: AnimationEntryMetadata[];
interpolation: [string, string];
constructor(
{templateUrl, template, directives, pipes, encapsulation, styles, styleUrls, animations}: {
{templateUrl, template, directives, pipes, encapsulation, styles, styleUrls, animations,
interpolation}: {
templateUrl?: string,
template?: string,
directives?: Array<Type|any[]>,
@ -137,7 +140,8 @@ export class ViewMetadata {
encapsulation?: ViewEncapsulation,
styles?: string[],
styleUrls?: string[],
animations?: AnimationEntryMetadata[]
animations?: AnimationEntryMetadata[],
interpolation?: [string, string]
} = {}) {
this.templateUrl = templateUrl;
this.template = template;
@ -147,5 +151,6 @@ export class ViewMetadata {
this.pipes = pipes;
this.encapsulation = encapsulation;
this.animations = animations;
this.interpolation = interpolation;
}
}

View File

@ -1300,6 +1300,31 @@ function declareTests({useJit}: {useJit: boolean}) {
async.done();
});
}));
it('should support custom interpolation',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
tcb.overrideView(
MyComp, new ViewMetadata({
template: `<div>{{ctxProp}}</div>
<cmp-with-custom-interpolation-a></cmp-with-custom-interpolation-a>
<cmp-with-custom-interpolation-b></cmp-with-custom-interpolation-b>`,
directives: [
ComponentWithCustomInterpolationA, ComponentWithCustomInterpolationB
]
}))
.createAsync(MyComp)
.then((fixture) => {
fixture.debugElement.componentInstance.ctxProp = 'Default Interpolation';
fixture.detectChanges();
expect(fixture.debugElement.nativeElement)
.toHaveText(
'Default Interpolation\nCustom Interpolation A\nCustom Interpolation B (Default Interpolation)');
async.done();
});
}));
});
describe('dependency injection', () => {
@ -2023,6 +2048,32 @@ function declareTests({useJit}: {useJit: boolean}) {
});
}
@Component({selector: 'cmp-with-default-interpolation', template: `{{text}}`})
class ComponentWithDefaultInterpolation {
text = 'Default Interpolation';
}
@Component({
selector: 'cmp-with-custom-interpolation-a',
template: `<div>{%text%}</div>`,
interpolation: ['{%', '%}']
})
class ComponentWithCustomInterpolationA {
text = 'Custom Interpolation A';
}
@Component({
selector: 'cmp-with-custom-interpolation-b',
template:
`<div>{**text%}</div> (<cmp-with-default-interpolation></cmp-with-default-interpolation>)`,
interpolation: ['{**', '%}'],
directives: [ComponentWithDefaultInterpolation]
})
class ComponentWithCustomInterpolationB {
text = 'Custom Interpolation B';
}
@Injectable()
class MyService {
greeting: string;

View File

@ -367,3 +367,7 @@ bool hasConstructor(Object value, Type type) {
String escape(String s) {
return Uri.encodeComponent(s);
}
String escapeRegExp(String s) {
return s.replaceAllMapped(new RegExp(r'([.*+?^=!:${}()|[\]\/\\])'), (Match m) => '\\${m[1]}');
}

View File

@ -466,3 +466,7 @@ export function hasConstructor(value: Object, type: Type): boolean {
export function escape(s: string): string {
return _global.encodeURI(s);
}
export function escapeRegExp(s: string): string {
return s.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
}

View File

@ -131,7 +131,7 @@ const CORE = [
'CollectionChangeRecord.toString():string',
'CollectionChangeRecord.trackById:any',
'ComponentDecorator',
'ComponentDecorator.View(obj:{templateUrl?:string, template?:string, directives?:Array<Type|any[]>, pipes?:Array<Type|any[]>, renderer?:string, styles?:string[], styleUrls?:string[], animations?:AnimationEntryMetadata[]}):ViewDecorator',
'ComponentDecorator.View(obj:{templateUrl?:string, template?:string, directives?:Array<Type|any[]>, pipes?:Array<Type|any[]>, renderer?:string, styles?:string[], styleUrls?:string[], animations?:AnimationEntryMetadata[], interpolation?:[string, string]}):ViewDecorator',
'ComponentFactory.componentType:Type',
'ComponentFactory.constructor(selector:string, _viewFactory:Function, _componentType:Type)',
'ComponentFactory.create(injector:Injector, projectableNodes:any[][]=null, rootSelectorOrNode:string|any=null):ComponentRef<C>',
@ -140,9 +140,10 @@ const CORE = [
'ComponentMetadata',
'ComponentMetadata.animations:AnimationEntryMetadata[]',
'ComponentMetadata.changeDetection:ChangeDetectionStrategy',
'ComponentMetadata.constructor({selector,inputs,outputs,properties,events,host,exportAs,moduleId,providers,viewProviders,changeDetection=ChangeDetectionStrategy.Default,queries,templateUrl,template,styleUrls,styles,animations,directives,pipes,encapsulation}:{selector?:string, inputs?:string[], outputs?:string[], properties?:string[], events?:string[], host?:{[key:string]:string}, providers?:any[], exportAs?:string, moduleId?:string, viewProviders?:any[], queries?:{[key:string]:any}, changeDetection?:ChangeDetectionStrategy, templateUrl?:string, template?:string, styleUrls?:string[], styles?:string[], animations?:AnimationEntryMetadata[], directives?:Array<Type|any[]>, pipes?:Array<Type|any[]>, encapsulation?:ViewEncapsulation}={})',
'ComponentMetadata.constructor({selector,inputs,outputs,properties,events,host,exportAs,moduleId,providers,viewProviders,changeDetection=ChangeDetectionStrategy.Default,queries,templateUrl,template,styleUrls,styles,animations,directives,pipes,encapsulation,interpolation}:{selector?:string, inputs?:string[], outputs?:string[], properties?:string[], events?:string[], host?:{[key:string]:string}, providers?:any[], exportAs?:string, moduleId?:string, viewProviders?:any[], queries?:{[key:string]:any}, changeDetection?:ChangeDetectionStrategy, templateUrl?:string, template?:string, styleUrls?:string[], styles?:string[], animations?:AnimationEntryMetadata[], directives?:Array<Type|any[]>, pipes?:Array<Type|any[]>, encapsulation?:ViewEncapsulation, interpolation?:[string, string]}={})',
'ComponentMetadata.directives:Array<Type|any[]>',
'ComponentMetadata.encapsulation:ViewEncapsulation',
'ComponentMetadata.interpolation:[string, string]',
'ComponentMetadata.moduleId:string',
'ComponentMetadata.pipes:Array<Type|any[]>',
'ComponentMetadata.styles:string[]',
@ -592,16 +593,17 @@ const CORE = [
'ViewContainerRef.parentInjector:Injector',
'ViewContainerRef.remove(index?:number):void',
'ViewDecorator',
'ViewDecorator.View(obj:{templateUrl?:string, template?:string, directives?:Array<Type|any[]>, pipes?:Array<Type|any[]>, renderer?:string, styles?:string[], styleUrls?:string[], animations?:AnimationEntryMetadata[]}):ViewDecorator',
'ViewDecorator.View(obj:{templateUrl?:string, template?:string, directives?:Array<Type|any[]>, pipes?:Array<Type|any[]>, renderer?:string, styles?:string[], styleUrls?:string[], animations?:AnimationEntryMetadata[], interpolation?:[string, string]}):ViewDecorator',
'ViewEncapsulation',
'ViewEncapsulation.Emulated',
'ViewEncapsulation.Native',
'ViewEncapsulation.None',
'ViewMetadata',
'ViewMetadata.animations:AnimationEntryMetadata[]',
'ViewMetadata.constructor({templateUrl,template,directives,pipes,encapsulation,styles,styleUrls,animations}:{templateUrl?:string, template?:string, directives?:Array<Type|any[]>, pipes?:Array<Type|any[]>, encapsulation?:ViewEncapsulation, styles?:string[], styleUrls?:string[], animations?:AnimationEntryMetadata[]}={})',
'ViewMetadata.constructor({templateUrl,template,directives,pipes,encapsulation,styles,styleUrls,animations,interpolation}:{templateUrl?:string, template?:string, directives?:Array<Type|any[]>, pipes?:Array<Type|any[]>, encapsulation?:ViewEncapsulation, styles?:string[], styleUrls?:string[], animations?:AnimationEntryMetadata[], interpolation?:[string, string]}={})',
'ViewMetadata.directives:Array<Type|any[]>',
'ViewMetadata.encapsulation:ViewEncapsulation',
'ViewMetadata.interpolation:[string, string]',
'ViewMetadata.pipes:Array<Type|any[]>',
'ViewMetadata.styles:string[]',
'ViewMetadata.styleUrls:string[]',
@ -1229,9 +1231,10 @@ const COMPILER = [
'CompilerConfig.useJit:boolean',
'CompileTemplateMetadata',
'CompileTemplateMetadata.animations:CompileAnimationEntryMetadata[]',
'CompileTemplateMetadata.constructor({encapsulation,template,templateUrl,styles,styleUrls,animations,ngContentSelectors}:{encapsulation?:ViewEncapsulation, template?:string, templateUrl?:string, styles?:string[], styleUrls?:string[], ngContentSelectors?:string[], animations?:CompileAnimationEntryMetadata[]}={})',
'CompileTemplateMetadata.constructor({encapsulation,template,templateUrl,styles,styleUrls,animations,ngContentSelectors,interpolation}:{encapsulation?:ViewEncapsulation, template?:string, templateUrl?:string, styles?:string[], styleUrls?:string[], ngContentSelectors?:string[], animations?:CompileAnimationEntryMetadata[], interpolation?:[string, string]}={})',
'CompileTemplateMetadata.encapsulation:ViewEncapsulation',
'CompileTemplateMetadata.fromJson(data:{[key:string]:any}):CompileTemplateMetadata',
'CompileTemplateMetadata.interpolation:[string, string]',
'CompileTemplateMetadata.ngContentSelectors:string[]',
'CompileTemplateMetadata.styles:string[]',
'CompileTemplateMetadata.styleUrls:string[]',