feat(compiler): make interpolation symbols configurable (@Component
config) (#9367)
closes #9158
This commit is contained in:
@ -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.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -104,7 +104,8 @@ export class DirectiveNormalizer {
|
||||
styles: allResolvedStyles,
|
||||
styleUrls: allStyleAbsUrls,
|
||||
ngContentSelectors: visitor.ngContentSelectors,
|
||||
animations: templateMeta.animations
|
||||
animations: templateMeta.animations,
|
||||
interpolation: templateMeta.interpolation
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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}'`);
|
||||
}
|
||||
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
@ -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>`;
|
||||
}
|
||||
|
9
modules/@angular/compiler/src/interpolation_config.ts
Normal file
9
modules/@angular/compiler/src/interpolation_config.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export interface InterpolationConfig {
|
||||
start: string;
|
||||
end: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_INTERPOLATION_CONFIG: InterpolationConfig = {
|
||||
start: '{{',
|
||||
end: '}}'
|
||||
};
|
@ -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)) {
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user