feat(compiler): make interpolation symbols configurable (@Component config) (#9367)
closes #9158
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user