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
@@ -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', () => {
@@ -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 }}'); });
@@ -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}`;
}
}
}
@@ -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));
}
@@ -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))] =
@@ -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 {
}
@@ -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;
}