fix(ivy): taking "interpolation" config option into account (FW-723) (#27363)
PR Close #27363
This commit is contained in:
parent
159788685a
commit
8e644d99fc
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ConstantPool, CssSelector, DomElementSchemaRegistry, ElementSchemaRegistry, Expression, R3ComponentMetadata, R3DirectiveMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
|
import {ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, ElementSchemaRegistry, Expression, InterpolationConfig, R3ComponentMetadata, R3DirectiveMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
@ -158,9 +158,22 @@ export class ComponentDecoratorHandler implements
|
||||||
}
|
}
|
||||||
}, undefined) !;
|
}, undefined) !;
|
||||||
|
|
||||||
|
let interpolation: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG;
|
||||||
|
if (component.has('interpolation')) {
|
||||||
|
const expr = component.get('interpolation') !;
|
||||||
|
const value = staticallyResolve(expr, this.reflector, this.checker);
|
||||||
|
if (!Array.isArray(value) || value.length !== 2 ||
|
||||||
|
!value.every(element => typeof element === 'string')) {
|
||||||
|
throw new FatalDiagnosticError(
|
||||||
|
ErrorCode.VALUE_HAS_WRONG_TYPE, expr,
|
||||||
|
'interpolation must be an array with 2 elements of string type');
|
||||||
|
}
|
||||||
|
interpolation = InterpolationConfig.fromArray(value as[string, string]);
|
||||||
|
}
|
||||||
|
|
||||||
const template = parseTemplate(
|
const template = parseTemplate(
|
||||||
templateStr, `${node.getSourceFile().fileName}#${node.name!.text}/template.html`,
|
templateStr, `${node.getSourceFile().fileName}#${node.name!.text}/template.html`,
|
||||||
{preserveWhitespaces});
|
{preserveWhitespaces, interpolationConfig: interpolation});
|
||||||
if (template.errors !== undefined) {
|
if (template.errors !== undefined) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Errors parsing template: ${template.errors.map(e => e.toString()).join(', ')}`);
|
`Errors parsing template: ${template.errors.map(e => e.toString()).join(', ')}`);
|
||||||
|
@ -230,6 +243,7 @@ export class ComponentDecoratorHandler implements
|
||||||
template,
|
template,
|
||||||
viewQueries,
|
viewQueries,
|
||||||
encapsulation,
|
encapsulation,
|
||||||
|
interpolation,
|
||||||
styles: styles || [],
|
styles: styles || [],
|
||||||
|
|
||||||
// These will be replaced during the compilation step, after all `NgModule`s have been
|
// These will be replaced during the compilation step, after all `NgModule`s have been
|
||||||
|
@ -276,7 +290,8 @@ export class ComponentDecoratorHandler implements
|
||||||
metadata = {...metadata, directives, pipes, wrapDirectivesAndPipesInClosure};
|
metadata = {...metadata, directives, pipes, wrapDirectivesAndPipesInClosure};
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = compileComponentFromMetadata(metadata, pool, makeBindingParser());
|
const res =
|
||||||
|
compileComponentFromMetadata(metadata, pool, makeBindingParser(metadata.interpolation));
|
||||||
|
|
||||||
const statements = res.statements;
|
const statements = res.statements;
|
||||||
if (analysis.metadataStmt !== null) {
|
if (analysis.metadataStmt !== null) {
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import {setup} from '@angular/compiler/test/aot/test_util';
|
import {setup} from '@angular/compiler/test/aot/test_util';
|
||||||
|
|
||||||
import {DEFAULT_INTERPOLATION_CONFIG} from '../../../compiler/src/compiler';
|
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../../compiler/src/compiler';
|
||||||
import {decimalDigest} from '../../../compiler/src/i18n/digest';
|
import {decimalDigest} from '../../../compiler/src/i18n/digest';
|
||||||
import {extractMessages} from '../../../compiler/src/i18n/extractor_merger';
|
import {extractMessages} from '../../../compiler/src/i18n/extractor_merger';
|
||||||
import {HtmlParser} from '../../../compiler/src/ml_parser/html_parser';
|
import {HtmlParser} from '../../../compiler/src/ml_parser/html_parser';
|
||||||
|
@ -37,33 +37,35 @@ const extract = (from: string, regex: any, transformFn: (match: any[]) => any) =
|
||||||
|
|
||||||
// verify that we extracted all the necessary translations
|
// verify that we extracted all the necessary translations
|
||||||
// and their ids match the ones extracted via 'ng xi18n'
|
// and their ids match the ones extracted via 'ng xi18n'
|
||||||
const verifyTranslationIds = (source: string, output: string, exceptions = {}) => {
|
const verifyTranslationIds =
|
||||||
const parseResult = htmlParser.parse(source, 'path:://to/template', true);
|
(source: string, output: string, exceptions = {},
|
||||||
const extractedIdToMsg = new Map<string, any>();
|
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG) => {
|
||||||
const extractedIds = new Set<string>();
|
const parseResult = htmlParser.parse(source, 'path:://to/template', true);
|
||||||
const generatedIds = new Set<string>();
|
const extractedIdToMsg = new Map<string, any>();
|
||||||
const msgs = extractMessages(parseResult.rootNodes, DEFAULT_INTERPOLATION_CONFIG, [], {});
|
const extractedIds = new Set<string>();
|
||||||
msgs.messages.forEach(msg => {
|
const generatedIds = new Set<string>();
|
||||||
const id = msg.id || decimalDigest(msg);
|
const msgs = extractMessages(parseResult.rootNodes, interpolationConfig, [], {});
|
||||||
extractedIds.add(id);
|
msgs.messages.forEach(msg => {
|
||||||
extractedIdToMsg.set(id, msg);
|
const id = msg.id || decimalDigest(msg);
|
||||||
});
|
extractedIds.add(id);
|
||||||
const regexp = /const\s*MSG_EXTERNAL_(.+?)\s*=\s*goog\.getMsg/g;
|
extractedIdToMsg.set(id, msg);
|
||||||
const ids = extract(output, regexp, v => v[1]);
|
});
|
||||||
ids.forEach(id => { generatedIds.add(id.split('$$')[0]); });
|
const regexp = /const\s*MSG_EXTERNAL_(.+?)\s*=\s*goog\.getMsg/g;
|
||||||
const delta = diff(extractedIds, generatedIds);
|
const ids = extract(output, regexp, v => v[1]);
|
||||||
if (delta.size) {
|
ids.forEach(id => { generatedIds.add(id.split('$$')[0]); });
|
||||||
// check if we have ids in exception list
|
const delta = diff(extractedIds, generatedIds);
|
||||||
const outstanding = diff(delta, new Set(Object.keys(exceptions)));
|
if (delta.size) {
|
||||||
if (outstanding.size) {
|
// check if we have ids in exception list
|
||||||
throw new Error(`
|
const outstanding = diff(delta, new Set(Object.keys(exceptions)));
|
||||||
|
if (outstanding.size) {
|
||||||
|
throw new Error(`
|
||||||
Extracted and generated IDs don't match, delta:
|
Extracted and generated IDs don't match, delta:
|
||||||
${JSON.stringify(Array.from(delta))}
|
${JSON.stringify(Array.from(delta))}
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// verify that placeholders in translation string match
|
// verify that placeholders in translation string match
|
||||||
// placeholders object defined as goog.getMsg function argument
|
// placeholders object defined as goog.getMsg function argument
|
||||||
|
@ -99,6 +101,7 @@ const getAppFilesWithTemplate = (template: string, args: any = {}) => ({
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-component',
|
selector: 'my-component',
|
||||||
${args.preserveWhitespaces ? 'preserveWhitespaces: true,' : ''}
|
${args.preserveWhitespaces ? 'preserveWhitespaces: true,' : ''}
|
||||||
|
${args.interpolation ? 'interpolation: ' + JSON.stringify(args.interpolation) + ', ' : ''}
|
||||||
template: \`${template}\`
|
template: \`${template}\`
|
||||||
})
|
})
|
||||||
export class MyComponent {}
|
export class MyComponent {}
|
||||||
|
@ -135,7 +138,11 @@ const verify = (input: string, output: string, extra: any = {}): void => {
|
||||||
// invoke with translation names based on external ids
|
// invoke with translation names based on external ids
|
||||||
result = compile(files, angularFiles, opts(true));
|
result = compile(files, angularFiles, opts(true));
|
||||||
maybePrint(result.source, extra.verbose);
|
maybePrint(result.source, extra.verbose);
|
||||||
expect(verifyTranslationIds(input, result.source, extra.exceptions)).toBe(true);
|
const interpolationConfig = extra.inputArgs && extra.inputArgs.interpolation ?
|
||||||
|
InterpolationConfig.fromArray(extra.inputArgs.interpolation) :
|
||||||
|
undefined;
|
||||||
|
expect(verifyTranslationIds(input, result.source, extra.exceptions, interpolationConfig))
|
||||||
|
.toBe(true);
|
||||||
expect(verifyPlaceholdersIntegrity(result.source)).toBe(true);
|
expect(verifyPlaceholdersIntegrity(result.source)).toBe(true);
|
||||||
expectEmit(result.source, output, 'Incorrect template');
|
expectEmit(result.source, output, 'Incorrect template');
|
||||||
};
|
};
|
||||||
|
@ -346,6 +353,33 @@ describe('i18n support in the view compiler', () => {
|
||||||
verify(input, output);
|
verify(input, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support interpolation with custom interpolation config', () => {
|
||||||
|
const input = `
|
||||||
|
<div i18n-title="m|d" title="intro {% valueA | uppercase %}"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const output = String.raw `
|
||||||
|
const $MSG_EXTERNAL_8977039798304050198$ = goog.getMsg("intro {$interpolation}", {
|
||||||
|
"interpolation": "\uFFFD0\uFFFD"
|
||||||
|
});
|
||||||
|
const $_c0$ = ["title", $MSG_EXTERNAL_8977039798304050198$];
|
||||||
|
…
|
||||||
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵelementStart(0, "div");
|
||||||
|
$r3$.ɵpipe(1, "uppercase");
|
||||||
|
$r3$.ɵi18nAttributes(2, $_c0$);
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(1, 0, ctx.valueA)));
|
||||||
|
$r3$.ɵi18nApply(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
verify(input, output, {inputArgs: {interpolation: ['{%', '%}']}});
|
||||||
|
});
|
||||||
|
|
||||||
it('should correctly bind to context in nested template', () => {
|
it('should correctly bind to context in nested template', () => {
|
||||||
const input = `
|
const input = `
|
||||||
<div *ngFor="let outer of items">
|
<div *ngFor="let outer of items">
|
||||||
|
@ -647,6 +681,31 @@ describe('i18n support in the view compiler', () => {
|
||||||
verify(input, output);
|
verify(input, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support interpolation with custom interpolation config', () => {
|
||||||
|
const input = `
|
||||||
|
<div i18n>{% valueA %}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const output = String.raw `
|
||||||
|
const $MSG_EXTERNAL_6749967533321674787$ = goog.getMsg("{$interpolation}", {
|
||||||
|
"interpolation": "\uFFFD0\uFFFD"
|
||||||
|
});
|
||||||
|
…
|
||||||
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵelementStart(0, "div");
|
||||||
|
$r3$.ɵi18n(1, $MSG_EXTERNAL_6749967533321674787$);
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind(ctx.valueA));
|
||||||
|
$r3$.ɵi18nApply(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
verify(input, output, {inputArgs: {interpolation: ['{%', '%}']}});
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle i18n attributes with bindings in content', () => {
|
it('should handle i18n attributes with bindings in content', () => {
|
||||||
const input = `
|
const input = `
|
||||||
<div i18n>My i18n block #{{ one }}</div>
|
<div i18n>My i18n block #{{ one }}</div>
|
||||||
|
@ -1685,6 +1744,33 @@ describe('i18n support in the view compiler', () => {
|
||||||
verify(input, output);
|
verify(input, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support interpolation with custom interpolation config', () => {
|
||||||
|
const input = `
|
||||||
|
<div i18n>{age, select, 10 {ten} 20 {twenty} other {{% other %}}}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const output = String.raw `
|
||||||
|
const $MSG_EXTERNAL_2949673783721159566$$RAW$ = goog.getMsg("{VAR_SELECT, select, 10 {ten} 20 {twenty} other {{$interpolation}}}", {
|
||||||
|
"interpolation": "\uFFFD1\uFFFD"
|
||||||
|
});
|
||||||
|
const $MSG_EXTERNAL_2949673783721159566$ = $r3$.ɵi18nPostprocess($MSG_EXTERNAL_2949673783721159566$$RAW$, { "VAR_SELECT": "\uFFFD0\uFFFD" });
|
||||||
|
…
|
||||||
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵelementStart(0, "div");
|
||||||
|
$r3$.ɵi18n(1, $MSG_EXTERNAL_2949673783721159566$);
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind(ctx.age));
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind(ctx.other));
|
||||||
|
$r3$.ɵi18nApply(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
verify(input, output, {inputArgs: {interpolation: ['{%', '%}']}});
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle icus with html', () => {
|
it('should handle icus with html', () => {
|
||||||
const input = `
|
const input = `
|
||||||
<div i18n>
|
<div i18n>
|
||||||
|
|
|
@ -683,6 +683,25 @@ describe('ngtsc behavioral tests', () => {
|
||||||
expect(jsContents).toContain('i18n(1, MSG_TEST_TS_0);');
|
expect(jsContents).toContain('i18n(1, MSG_TEST_TS_0);');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('@Component\'s `interpolation` should override default interpolation config', () => {
|
||||||
|
env.tsconfig();
|
||||||
|
env.write(`test.ts`, `
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
@Component({
|
||||||
|
selector: 'cmp-with-custom-interpolation-a',
|
||||||
|
template: \`<div>{%text%}</div>\`,
|
||||||
|
interpolation: ['{%', '%}']
|
||||||
|
})
|
||||||
|
class ComponentWithCustomInterpolationA {
|
||||||
|
text = 'Custom Interpolation A';
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
env.driveMain();
|
||||||
|
const jsContents = env.getContents('test.js');
|
||||||
|
expect(jsContents).toContain('interpolation1("", ctx.text, "")');
|
||||||
|
});
|
||||||
|
|
||||||
it('should correctly recognize local symbols', () => {
|
it('should correctly recognize local symbols', () => {
|
||||||
env.tsconfig();
|
env.tsconfig();
|
||||||
env.write('module.ts', `
|
env.write('module.ts', `
|
||||||
|
|
|
@ -132,6 +132,7 @@ export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade {
|
||||||
styles: string[];
|
styles: string[];
|
||||||
encapsulation: ViewEncapsulation;
|
encapsulation: ViewEncapsulation;
|
||||||
viewProviders: Provider[]|null;
|
viewProviders: Provider[]|null;
|
||||||
|
interpolation?: [string, string];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ViewEncapsulation = number;
|
export type ViewEncapsulation = number;
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {CompilerFacade, CoreEnvironment, ExportedCompilerFacade, R3ComponentMeta
|
||||||
import {ConstantPool} from './constant_pool';
|
import {ConstantPool} from './constant_pool';
|
||||||
import {HostBinding, HostListener, Input, Output, Type} from './core';
|
import {HostBinding, HostListener, Input, Output, Type} from './core';
|
||||||
import {compileInjectable} from './injectable_compiler_2';
|
import {compileInjectable} from './injectable_compiler_2';
|
||||||
|
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './ml_parser/interpolation_config';
|
||||||
import {Expression, LiteralExpr, WrappedNodeExpr} from './output/output_ast';
|
import {Expression, LiteralExpr, WrappedNodeExpr} from './output/output_ast';
|
||||||
import {R3DependencyMetadata, R3ResolvedDependencyType} from './render3/r3_factory';
|
import {R3DependencyMetadata, R3ResolvedDependencyType} from './render3/r3_factory';
|
||||||
import {jitExpression} from './render3/r3_jit';
|
import {jitExpression} from './render3/r3_jit';
|
||||||
|
@ -103,10 +104,13 @@ export class CompilerFacadeImpl implements CompilerFacade {
|
||||||
// The ConstantPool is a requirement of the JIT'er.
|
// The ConstantPool is a requirement of the JIT'er.
|
||||||
const constantPool = new ConstantPool();
|
const constantPool = new ConstantPool();
|
||||||
|
|
||||||
|
const interpolationConfig = facade.interpolation ?
|
||||||
|
InterpolationConfig.fromArray(facade.interpolation) :
|
||||||
|
DEFAULT_INTERPOLATION_CONFIG;
|
||||||
// Parse the template and check for errors.
|
// Parse the template and check for errors.
|
||||||
const template = parseTemplate(facade.template, sourceMapUrl, {
|
const template = parseTemplate(
|
||||||
preserveWhitespaces: facade.preserveWhitespaces || false,
|
facade.template, sourceMapUrl,
|
||||||
});
|
{preserveWhitespaces: facade.preserveWhitespaces || false, interpolationConfig});
|
||||||
if (template.errors !== undefined) {
|
if (template.errors !== undefined) {
|
||||||
const errors = template.errors.map(err => err.toString()).join(', ');
|
const errors = template.errors.map(err => err.toString()).join(', ');
|
||||||
throw new Error(`Errors during JIT compilation of template for ${facade.name}: ${errors}`);
|
throw new Error(`Errors during JIT compilation of template for ${facade.name}: ${errors}`);
|
||||||
|
@ -124,13 +128,14 @@ export class CompilerFacadeImpl implements CompilerFacade {
|
||||||
wrapDirectivesAndPipesInClosure: false,
|
wrapDirectivesAndPipesInClosure: false,
|
||||||
styles: facade.styles || [],
|
styles: facade.styles || [],
|
||||||
encapsulation: facade.encapsulation as any,
|
encapsulation: facade.encapsulation as any,
|
||||||
|
interpolation: interpolationConfig,
|
||||||
animations: facade.animations != null ? new WrappedNodeExpr(facade.animations) : null,
|
animations: facade.animations != null ? new WrappedNodeExpr(facade.animations) : null,
|
||||||
viewProviders: facade.viewProviders != null ? new WrappedNodeExpr(facade.viewProviders) :
|
viewProviders: facade.viewProviders != null ? new WrappedNodeExpr(facade.viewProviders) :
|
||||||
null,
|
null,
|
||||||
relativeContextFilePath: '',
|
relativeContextFilePath: '',
|
||||||
i18nUseExternalIds: true,
|
i18nUseExternalIds: true,
|
||||||
},
|
},
|
||||||
constantPool, makeBindingParser());
|
constantPool, makeBindingParser(interpolationConfig));
|
||||||
const preStatements = [...constantPool.statements, ...res.statements];
|
const preStatements = [...constantPool.statements, ...res.statements];
|
||||||
|
|
||||||
return jitExpression(res.expression, angularCoreEnv, sourceMapUrl, preStatements);
|
return jitExpression(res.expression, angularCoreEnv, sourceMapUrl, preStatements);
|
||||||
|
|
|
@ -230,8 +230,11 @@ class HtmlAstToIvyAst implements html.Visitor {
|
||||||
Object.keys(meta.placeholders).forEach(key => {
|
Object.keys(meta.placeholders).forEach(key => {
|
||||||
const value = meta.placeholders[key];
|
const value = meta.placeholders[key];
|
||||||
if (key.startsWith(I18N_ICU_VAR_PREFIX)) {
|
if (key.startsWith(I18N_ICU_VAR_PREFIX)) {
|
||||||
vars[key] =
|
const config = this.bindingParser.interpolationConfig;
|
||||||
this._visitTextWithInterpolation(`{{${value}}}`, expansion.sourceSpan) as t.BoundText;
|
// ICU expression is a plain string, not wrapped into start
|
||||||
|
// and end tags, so we wrap it before passing to binding parser
|
||||||
|
const wrapped = `${config.start}${value}${config.end}`;
|
||||||
|
vars[key] = this._visitTextWithInterpolation(wrapped, expansion.sourceSpan) as t.BoundText;
|
||||||
} else {
|
} else {
|
||||||
placeholders[key] = this._visitTextWithInterpolation(value, expansion.sourceSpan);
|
placeholders[key] = this._visitTextWithInterpolation(value, expansion.sourceSpan);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ViewEncapsulation} from '../../core';
|
import {ViewEncapsulation} from '../../core';
|
||||||
|
import {InterpolationConfig} from '../../ml_parser/interpolation_config';
|
||||||
import * as o from '../../output/output_ast';
|
import * as o from '../../output/output_ast';
|
||||||
import {ParseSourceSpan} from '../../parse_util';
|
import {ParseSourceSpan} from '../../parse_util';
|
||||||
import * as t from '../r3_ast';
|
import * as t from '../r3_ast';
|
||||||
|
@ -184,7 +185,6 @@ export interface R3ComponentMetadata extends R3DirectiveMetadata {
|
||||||
*/
|
*/
|
||||||
viewProviders: o.Expression|null;
|
viewProviders: o.Expression|null;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Path to the .ts file in which this template's generated code will be included, relative to
|
* Path to the .ts file in which this template's generated code will be included, relative to
|
||||||
* the compilation root. This will be used to generate identifiers that need to be globally
|
* the compilation root. This will be used to generate identifiers that need to be globally
|
||||||
|
@ -197,6 +197,11 @@ export interface R3ComponentMetadata extends R3DirectiveMetadata {
|
||||||
* (used by Closure Compiler's output of `goog.getMsg` for transition period)
|
* (used by Closure Compiler's output of `goog.getMsg` for transition period)
|
||||||
*/
|
*/
|
||||||
i18nUseExternalIds: boolean;
|
i18nUseExternalIds: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides the default interpolation start and end delimiters ({{ and }})
|
||||||
|
*/
|
||||||
|
interpolation: InterpolationConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {ConstantPool, DefinitionKind} from '../../constant_pool';
|
||||||
import * as core from '../../core';
|
import * as core from '../../core';
|
||||||
import {AST, ParsedEvent} from '../../expression_parser/ast';
|
import {AST, ParsedEvent} from '../../expression_parser/ast';
|
||||||
import {LifecycleHooks} from '../../lifecycle_reflector';
|
import {LifecycleHooks} from '../../lifecycle_reflector';
|
||||||
|
import {DEFAULT_INTERPOLATION_CONFIG} from '../../ml_parser/interpolation_config';
|
||||||
import * as o from '../../output/output_ast';
|
import * as o from '../../output/output_ast';
|
||||||
import {typeSourceSpan} from '../../parse_util';
|
import {typeSourceSpan} from '../../parse_util';
|
||||||
import {CssSelector, SelectorMatcher} from '../../selector';
|
import {CssSelector, SelectorMatcher} from '../../selector';
|
||||||
|
@ -382,6 +383,7 @@ export function compileComponentFromRender2(
|
||||||
styles: (summary.template && summary.template.styles) || EMPTY_ARRAY,
|
styles: (summary.template && summary.template.styles) || EMPTY_ARRAY,
|
||||||
encapsulation:
|
encapsulation:
|
||||||
(summary.template && summary.template.encapsulation) || core.ViewEncapsulation.Emulated,
|
(summary.template && summary.template.encapsulation) || core.ViewEncapsulation.Emulated,
|
||||||
|
interpolation: DEFAULT_INTERPOLATION_CONFIG,
|
||||||
animations: null,
|
animations: null,
|
||||||
viewProviders:
|
viewProviders:
|
||||||
component.viewProviders.length > 0 ? new o.WrappedNodeExpr(component.viewProviders) : null,
|
component.viewProviders.length > 0 ? new o.WrappedNodeExpr(component.viewProviders) : null,
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {decimalDigest} from '../../../i18n/digest';
|
||||||
import * as i18n from '../../../i18n/i18n_ast';
|
import * as i18n from '../../../i18n/i18n_ast';
|
||||||
import {createI18nMessageFactory} from '../../../i18n/i18n_parser';
|
import {createI18nMessageFactory} from '../../../i18n/i18n_parser';
|
||||||
import * as html from '../../../ml_parser/ast';
|
import * as html from '../../../ml_parser/ast';
|
||||||
import {DEFAULT_INTERPOLATION_CONFIG} from '../../../ml_parser/interpolation_config';
|
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../../ml_parser/interpolation_config';
|
||||||
import {ParseTreeResult} from '../../../ml_parser/parser';
|
import {ParseTreeResult} from '../../../ml_parser/parser';
|
||||||
|
|
||||||
import {I18N_ATTR, I18N_ATTR_PREFIX, I18nMeta, hasI18nAttrs, icuFromI18nMessage, metaFromI18nMessage, parseI18nMeta} from './util';
|
import {I18N_ATTR, I18N_ATTR_PREFIX, I18nMeta, hasI18nAttrs, icuFromI18nMessage, metaFromI18nMessage, parseI18nMeta} from './util';
|
||||||
|
@ -25,10 +25,14 @@ function setI18nRefs(html: html.Node & {i18n: i18n.AST}, i18n: i18n.Node) {
|
||||||
* stored with other element's and attribute's information.
|
* stored with other element's and attribute's information.
|
||||||
*/
|
*/
|
||||||
export class I18nMetaVisitor implements html.Visitor {
|
export class I18nMetaVisitor implements html.Visitor {
|
||||||
// i18n message generation factory
|
private _createI18nMessage: any;
|
||||||
private _createI18nMessage = createI18nMessageFactory(DEFAULT_INTERPOLATION_CONFIG);
|
|
||||||
|
|
||||||
constructor(private config: {keepI18nAttrs: boolean}) {}
|
constructor(
|
||||||
|
private interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG,
|
||||||
|
private keepI18nAttrs: boolean = false) {
|
||||||
|
// i18n message generation factory
|
||||||
|
this._createI18nMessage = createI18nMessageFactory(interpolationConfig);
|
||||||
|
}
|
||||||
|
|
||||||
private _generateI18nMessage(
|
private _generateI18nMessage(
|
||||||
nodes: html.Node[], meta: string|i18n.AST = '',
|
nodes: html.Node[], meta: string|i18n.AST = '',
|
||||||
|
@ -81,7 +85,7 @@ export class I18nMetaVisitor implements html.Visitor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.config.keepI18nAttrs) {
|
if (!this.keepI18nAttrs) {
|
||||||
// update element's attributes,
|
// update element's attributes,
|
||||||
// keeping only non-i18n related ones
|
// keeping only non-i18n related ones
|
||||||
element.attrs = attrs;
|
element.attrs = attrs;
|
||||||
|
@ -116,8 +120,12 @@ export class I18nMetaVisitor implements html.Visitor {
|
||||||
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any { return expansionCase; }
|
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any { return expansionCase; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processI18nMeta(htmlAstWithErrors: ParseTreeResult): ParseTreeResult {
|
export function processI18nMeta(
|
||||||
|
htmlAstWithErrors: ParseTreeResult,
|
||||||
|
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ParseTreeResult {
|
||||||
return new ParseTreeResult(
|
return new ParseTreeResult(
|
||||||
html.visitAll(new I18nMetaVisitor({keepI18nAttrs: false}), htmlAstWithErrors.rootNodes),
|
html.visitAll(
|
||||||
|
new I18nMetaVisitor(interpolationConfig, /* keepI18nAttrs */ false),
|
||||||
|
htmlAstWithErrors.rootNodes),
|
||||||
htmlAstWithErrors.errors);
|
htmlAstWithErrors.errors);
|
||||||
}
|
}
|
|
@ -17,7 +17,7 @@ import * as i18n from '../../i18n/i18n_ast';
|
||||||
import * as html from '../../ml_parser/ast';
|
import * as html from '../../ml_parser/ast';
|
||||||
import {HtmlParser} from '../../ml_parser/html_parser';
|
import {HtmlParser} from '../../ml_parser/html_parser';
|
||||||
import {WhitespaceVisitor} from '../../ml_parser/html_whitespaces';
|
import {WhitespaceVisitor} from '../../ml_parser/html_whitespaces';
|
||||||
import {DEFAULT_INTERPOLATION_CONFIG} from '../../ml_parser/interpolation_config';
|
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../ml_parser/interpolation_config';
|
||||||
import {isNgContainer as checkIsNgContainer, splitNsName} from '../../ml_parser/tags';
|
import {isNgContainer as checkIsNgContainer, splitNsName} from '../../ml_parser/tags';
|
||||||
import {mapLiteral} from '../../output/map_util';
|
import {mapLiteral} from '../../output/map_util';
|
||||||
import * as o from '../../output/output_ast';
|
import * as o from '../../output/output_ast';
|
||||||
|
@ -1396,11 +1396,13 @@ function interpolate(args: o.Expression[]): o.Expression {
|
||||||
* @param templateUrl URL to use for source mapping of the parsed template
|
* @param templateUrl URL to use for source mapping of the parsed template
|
||||||
*/
|
*/
|
||||||
export function parseTemplate(
|
export function parseTemplate(
|
||||||
template: string, templateUrl: string, options: {preserveWhitespaces?: boolean}):
|
template: string, templateUrl: string,
|
||||||
|
options: {preserveWhitespaces?: boolean, interpolationConfig?: InterpolationConfig} = {}):
|
||||||
{errors?: ParseError[], nodes: t.Node[], hasNgContent: boolean, ngContentSelectors: string[]} {
|
{errors?: ParseError[], nodes: t.Node[], hasNgContent: boolean, ngContentSelectors: string[]} {
|
||||||
const bindingParser = makeBindingParser();
|
const {interpolationConfig, preserveWhitespaces} = options;
|
||||||
|
const bindingParser = makeBindingParser(interpolationConfig);
|
||||||
const htmlParser = new HtmlParser();
|
const htmlParser = new HtmlParser();
|
||||||
const parseResult = htmlParser.parse(template, templateUrl, true);
|
const parseResult = htmlParser.parse(template, templateUrl, true, interpolationConfig);
|
||||||
|
|
||||||
if (parseResult.errors && parseResult.errors.length > 0) {
|
if (parseResult.errors && parseResult.errors.length > 0) {
|
||||||
return {errors: parseResult.errors, nodes: [], hasNgContent: false, ngContentSelectors: []};
|
return {errors: parseResult.errors, nodes: [], hasNgContent: false, ngContentSelectors: []};
|
||||||
|
@ -1412,17 +1414,18 @@ export function parseTemplate(
|
||||||
// before we run whitespace removal process, because existing i18n
|
// before we run whitespace removal process, because existing i18n
|
||||||
// extraction process (ng xi18n) relies on a raw content to generate
|
// extraction process (ng xi18n) relies on a raw content to generate
|
||||||
// message ids
|
// message ids
|
||||||
const i18nConfig = {keepI18nAttrs: !options.preserveWhitespaces};
|
rootNodes =
|
||||||
rootNodes = html.visitAll(new I18nMetaVisitor(i18nConfig), rootNodes);
|
html.visitAll(new I18nMetaVisitor(interpolationConfig, !preserveWhitespaces), rootNodes);
|
||||||
|
|
||||||
if (!options.preserveWhitespaces) {
|
if (!preserveWhitespaces) {
|
||||||
rootNodes = html.visitAll(new WhitespaceVisitor(), rootNodes);
|
rootNodes = html.visitAll(new WhitespaceVisitor(), rootNodes);
|
||||||
|
|
||||||
// run i18n meta visitor again in case we remove whitespaces, because
|
// run i18n meta visitor again in case we remove whitespaces, because
|
||||||
// that might affect generated i18n message content. During this pass
|
// that might affect generated i18n message content. During this pass
|
||||||
// i18n IDs generated at the first pass will be preserved, so we can mimic
|
// i18n IDs generated at the first pass will be preserved, so we can mimic
|
||||||
// existing extraction process (ng xi18n)
|
// existing extraction process (ng xi18n)
|
||||||
rootNodes = html.visitAll(new I18nMetaVisitor({keepI18nAttrs: false}), rootNodes);
|
rootNodes = html.visitAll(
|
||||||
|
new I18nMetaVisitor(interpolationConfig, /* keepI18nAttrs */ false), rootNodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
const {nodes, hasNgContent, ngContentSelectors, errors} =
|
const {nodes, hasNgContent, ngContentSelectors, errors} =
|
||||||
|
@ -1437,10 +1440,10 @@ export function parseTemplate(
|
||||||
/**
|
/**
|
||||||
* Construct a `BindingParser` with a default configuration.
|
* Construct a `BindingParser` with a default configuration.
|
||||||
*/
|
*/
|
||||||
export function makeBindingParser(): BindingParser {
|
export function makeBindingParser(
|
||||||
|
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): BindingParser {
|
||||||
return new BindingParser(
|
return new BindingParser(
|
||||||
new Parser(new Lexer()), DEFAULT_INTERPOLATION_CONFIG, new DomElementSchemaRegistry(), null,
|
new Parser(new Lexer()), interpolationConfig, new DomElementSchemaRegistry(), null, []);
|
||||||
[]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveSanitizationFn(input: t.BoundAttribute, context: core.SecurityContext) {
|
function resolveSanitizationFn(input: t.BoundAttribute, context: core.SecurityContext) {
|
||||||
|
|
|
@ -45,6 +45,8 @@ export class BindingParser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get interpolationConfig(): InterpolationConfig { return this._interpolationConfig; }
|
||||||
|
|
||||||
getUsedPipes(): CompilePipeSummary[] { return Array.from(this._usedPipes.values()); }
|
getUsedPipes(): CompilePipeSummary[] { return Array.from(this._usedPipes.values()); }
|
||||||
|
|
||||||
createBoundHostProperties(dirMeta: CompileDirectiveSummary, sourceSpan: ParseSourceSpan):
|
createBoundHostProperties(dirMeta: CompileDirectiveSummary, sourceSpan: ParseSourceSpan):
|
||||||
|
|
|
@ -132,6 +132,7 @@ export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade {
|
||||||
styles: string[];
|
styles: string[];
|
||||||
encapsulation: ViewEncapsulation;
|
encapsulation: ViewEncapsulation;
|
||||||
viewProviders: Provider[]|null;
|
viewProviders: Provider[]|null;
|
||||||
|
interpolation?: [string, string];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ViewEncapsulation = number;
|
export type ViewEncapsulation = number;
|
||||||
|
|
|
@ -61,6 +61,7 @@ export function compileComponent(type: Type<any>, metadata: Component): void {
|
||||||
directives: [],
|
directives: [],
|
||||||
pipes: new Map(),
|
pipes: new Map(),
|
||||||
encapsulation: metadata.encapsulation || ViewEncapsulation.Emulated,
|
encapsulation: metadata.encapsulation || ViewEncapsulation.Emulated,
|
||||||
|
interpolation: metadata.interpolation,
|
||||||
viewProviders: metadata.viewProviders || null,
|
viewProviders: metadata.viewProviders || null,
|
||||||
};
|
};
|
||||||
ngComponentDef = compiler.compileComponent(
|
ngComponentDef = compiler.compileComponent(
|
||||||
|
|
|
@ -1295,27 +1295,26 @@ function declareTests(config?: {useJit: boolean}) {
|
||||||
expect(needsAttribute.fooAttribute).toBeNull();
|
expect(needsAttribute.fooAttribute).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
fixmeIvy('FW-723: Custom interpolation markers are not supported') &&
|
it('should support custom interpolation', () => {
|
||||||
it('should support custom interpolation', () => {
|
TestBed.configureTestingModule({
|
||||||
TestBed.configureTestingModule({
|
declarations: [
|
||||||
declarations: [
|
MyComp, ComponentWithCustomInterpolationA, ComponentWithCustomInterpolationB,
|
||||||
MyComp, ComponentWithCustomInterpolationA, ComponentWithCustomInterpolationB,
|
ComponentWithDefaultInterpolation
|
||||||
ComponentWithDefaultInterpolation
|
]
|
||||||
]
|
});
|
||||||
});
|
const template = `<div>{{ctxProp}}</div>
|
||||||
const template = `<div>{{ctxProp}}</div>
|
|
||||||
<cmp-with-custom-interpolation-a></cmp-with-custom-interpolation-a>
|
<cmp-with-custom-interpolation-a></cmp-with-custom-interpolation-a>
|
||||||
<cmp-with-custom-interpolation-b></cmp-with-custom-interpolation-b>`;
|
<cmp-with-custom-interpolation-b></cmp-with-custom-interpolation-b>`;
|
||||||
TestBed.overrideComponent(MyComp, {set: {template}});
|
TestBed.overrideComponent(MyComp, {set: {template}});
|
||||||
const fixture = TestBed.createComponent(MyComp);
|
const fixture = TestBed.createComponent(MyComp);
|
||||||
|
|
||||||
fixture.componentInstance.ctxProp = 'Default Interpolation';
|
fixture.componentInstance.ctxProp = 'Default Interpolation';
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(fixture.nativeElement)
|
expect(fixture.nativeElement)
|
||||||
.toHaveText(
|
.toHaveText(
|
||||||
'Default InterpolationCustom Interpolation ACustom Interpolation B (Default Interpolation)');
|
'Default InterpolationCustom Interpolation ACustom Interpolation B (Default Interpolation)');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('dependency injection', () => {
|
describe('dependency injection', () => {
|
||||||
|
|
Loading…
Reference in New Issue