From 8e644d99fcc69d2712ff2fe403a262fcc4dc7223 Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Thu, 29 Nov 2018 16:21:16 -0800 Subject: [PATCH] fix(ivy): taking "interpolation" config option into account (FW-723) (#27363) PR Close #27363 --- .../src/ngtsc/annotations/src/component.ts | 21 ++- .../compliance/r3_view_compiler_i18n_spec.ts | 138 ++++++++++++++---- .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 19 +++ .../compiler/src/compiler_facade_interface.ts | 1 + packages/compiler/src/jit_compiler_facade.ts | 13 +- .../src/render3/r3_template_transform.ts | 7 +- packages/compiler/src/render3/view/api.ts | 7 +- .../compiler/src/render3/view/compiler.ts | 2 + .../compiler/src/render3/view/i18n/meta.ts | 22 ++- .../compiler/src/render3/view/template.ts | 25 ++-- .../src/template_parser/binding_parser.ts | 2 + .../render3/jit/compiler_facade_interface.ts | 1 + packages/core/src/render3/jit/directive.ts | 1 + packages/core/test/linker/integration_spec.ts | 33 ++--- 14 files changed, 221 insertions(+), 71 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 9010b3c757..ffa0da9954 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -6,7 +6,7 @@ * 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 ts from 'typescript'; @@ -158,9 +158,22 @@ export class ComponentDecoratorHandler implements } }, 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( templateStr, `${node.getSourceFile().fileName}#${node.name!.text}/template.html`, - {preserveWhitespaces}); + {preserveWhitespaces, interpolationConfig: interpolation}); if (template.errors !== undefined) { throw new Error( `Errors parsing template: ${template.errors.map(e => e.toString()).join(', ')}`); @@ -230,6 +243,7 @@ export class ComponentDecoratorHandler implements template, viewQueries, encapsulation, + interpolation, styles: styles || [], // 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}; } - const res = compileComponentFromMetadata(metadata, pool, makeBindingParser()); + const res = + compileComponentFromMetadata(metadata, pool, makeBindingParser(metadata.interpolation)); const statements = res.statements; if (analysis.metadataStmt !== null) { diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts index 248e668f8e..04495bd4e5 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts @@ -8,7 +8,7 @@ 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 {extractMessages} from '../../../compiler/src/i18n/extractor_merger'; 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 // and their ids match the ones extracted via 'ng xi18n' -const verifyTranslationIds = (source: string, output: string, exceptions = {}) => { - const parseResult = htmlParser.parse(source, 'path:://to/template', true); - const extractedIdToMsg = new Map(); - const extractedIds = new Set(); - const generatedIds = new Set(); - const msgs = extractMessages(parseResult.rootNodes, DEFAULT_INTERPOLATION_CONFIG, [], {}); - msgs.messages.forEach(msg => { - const id = msg.id || decimalDigest(msg); - extractedIds.add(id); - extractedIdToMsg.set(id, msg); - }); - const regexp = /const\s*MSG_EXTERNAL_(.+?)\s*=\s*goog\.getMsg/g; - const ids = extract(output, regexp, v => v[1]); - ids.forEach(id => { generatedIds.add(id.split('$$')[0]); }); - const delta = diff(extractedIds, generatedIds); - if (delta.size) { - // check if we have ids in exception list - const outstanding = diff(delta, new Set(Object.keys(exceptions))); - if (outstanding.size) { - throw new Error(` +const verifyTranslationIds = + (source: string, output: string, exceptions = {}, + interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG) => { + const parseResult = htmlParser.parse(source, 'path:://to/template', true); + const extractedIdToMsg = new Map(); + const extractedIds = new Set(); + const generatedIds = new Set(); + const msgs = extractMessages(parseResult.rootNodes, interpolationConfig, [], {}); + msgs.messages.forEach(msg => { + const id = msg.id || decimalDigest(msg); + extractedIds.add(id); + extractedIdToMsg.set(id, msg); + }); + const regexp = /const\s*MSG_EXTERNAL_(.+?)\s*=\s*goog\.getMsg/g; + const ids = extract(output, regexp, v => v[1]); + ids.forEach(id => { generatedIds.add(id.split('$$')[0]); }); + const delta = diff(extractedIds, generatedIds); + if (delta.size) { + // check if we have ids in exception list + const outstanding = diff(delta, new Set(Object.keys(exceptions))); + if (outstanding.size) { + throw new Error(` Extracted and generated IDs don't match, delta: ${JSON.stringify(Array.from(delta))} `); - } - } - return true; -}; + } + } + return true; + }; // verify that placeholders in translation string match // placeholders object defined as goog.getMsg function argument @@ -99,6 +101,7 @@ const getAppFilesWithTemplate = (template: string, args: any = {}) => ({ @Component({ selector: 'my-component', ${args.preserveWhitespaces ? 'preserveWhitespaces: true,' : ''} + ${args.interpolation ? 'interpolation: ' + JSON.stringify(args.interpolation) + ', ' : ''} template: \`${template}\` }) export class MyComponent {} @@ -135,7 +138,11 @@ const verify = (input: string, output: string, extra: any = {}): void => { // invoke with translation names based on external ids result = compile(files, angularFiles, opts(true)); 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); expectEmit(result.source, output, 'Incorrect template'); }; @@ -346,6 +353,33 @@ describe('i18n support in the view compiler', () => { verify(input, output); }); + it('should support interpolation with custom interpolation config', () => { + const input = ` +
+ `; + + 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', () => { const input = `
@@ -647,6 +681,31 @@ describe('i18n support in the view compiler', () => { verify(input, output); }); + it('should support interpolation with custom interpolation config', () => { + const input = ` +
{% valueA %}
+ `; + + 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', () => { const input = `
My i18n block #{{ one }}
@@ -1685,6 +1744,33 @@ describe('i18n support in the view compiler', () => { verify(input, output); }); + it('should support interpolation with custom interpolation config', () => { + const input = ` +
{age, select, 10 {ten} 20 {twenty} other {{% other %}}}
+ `; + + 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', () => { const input = `
diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index d2601ee479..6d91e93663 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -683,6 +683,25 @@ describe('ngtsc behavioral tests', () => { 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: \`
{%text%}
\`, + 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', () => { env.tsconfig(); env.write('module.ts', ` diff --git a/packages/compiler/src/compiler_facade_interface.ts b/packages/compiler/src/compiler_facade_interface.ts index 0c7b004d8f..5665359c73 100644 --- a/packages/compiler/src/compiler_facade_interface.ts +++ b/packages/compiler/src/compiler_facade_interface.ts @@ -132,6 +132,7 @@ export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade { styles: string[]; encapsulation: ViewEncapsulation; viewProviders: Provider[]|null; + interpolation?: [string, string]; } export type ViewEncapsulation = number; diff --git a/packages/compiler/src/jit_compiler_facade.ts b/packages/compiler/src/jit_compiler_facade.ts index eab07d8423..e89c5d6691 100644 --- a/packages/compiler/src/jit_compiler_facade.ts +++ b/packages/compiler/src/jit_compiler_facade.ts @@ -11,6 +11,7 @@ import {CompilerFacade, CoreEnvironment, ExportedCompilerFacade, R3ComponentMeta import {ConstantPool} from './constant_pool'; import {HostBinding, HostListener, Input, Output, Type} from './core'; 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 {R3DependencyMetadata, R3ResolvedDependencyType} from './render3/r3_factory'; import {jitExpression} from './render3/r3_jit'; @@ -103,10 +104,13 @@ export class CompilerFacadeImpl implements CompilerFacade { // The ConstantPool is a requirement of the JIT'er. const constantPool = new ConstantPool(); + const interpolationConfig = facade.interpolation ? + InterpolationConfig.fromArray(facade.interpolation) : + DEFAULT_INTERPOLATION_CONFIG; // Parse the template and check for errors. - const template = parseTemplate(facade.template, sourceMapUrl, { - preserveWhitespaces: facade.preserveWhitespaces || false, - }); + const template = parseTemplate( + facade.template, sourceMapUrl, + {preserveWhitespaces: facade.preserveWhitespaces || false, interpolationConfig}); if (template.errors !== undefined) { const errors = template.errors.map(err => err.toString()).join(', '); throw new Error(`Errors during JIT compilation of template for ${facade.name}: ${errors}`); @@ -124,13 +128,14 @@ export class CompilerFacadeImpl implements CompilerFacade { wrapDirectivesAndPipesInClosure: false, styles: facade.styles || [], encapsulation: facade.encapsulation as any, + interpolation: interpolationConfig, animations: facade.animations != null ? new WrappedNodeExpr(facade.animations) : null, viewProviders: facade.viewProviders != null ? new WrappedNodeExpr(facade.viewProviders) : null, relativeContextFilePath: '', i18nUseExternalIds: true, }, - constantPool, makeBindingParser()); + constantPool, makeBindingParser(interpolationConfig)); const preStatements = [...constantPool.statements, ...res.statements]; return jitExpression(res.expression, angularCoreEnv, sourceMapUrl, preStatements); diff --git a/packages/compiler/src/render3/r3_template_transform.ts b/packages/compiler/src/render3/r3_template_transform.ts index 1fab7c15e8..9bd4d4caa1 100644 --- a/packages/compiler/src/render3/r3_template_transform.ts +++ b/packages/compiler/src/render3/r3_template_transform.ts @@ -230,8 +230,11 @@ class HtmlAstToIvyAst implements html.Visitor { Object.keys(meta.placeholders).forEach(key => { const value = meta.placeholders[key]; if (key.startsWith(I18N_ICU_VAR_PREFIX)) { - vars[key] = - this._visitTextWithInterpolation(`{{${value}}}`, expansion.sourceSpan) as t.BoundText; + const config = this.bindingParser.interpolationConfig; + // 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 { placeholders[key] = this._visitTextWithInterpolation(value, expansion.sourceSpan); } diff --git a/packages/compiler/src/render3/view/api.ts b/packages/compiler/src/render3/view/api.ts index 9cb0ce3ef2..8fb10cc046 100644 --- a/packages/compiler/src/render3/view/api.ts +++ b/packages/compiler/src/render3/view/api.ts @@ -7,6 +7,7 @@ */ import {ViewEncapsulation} from '../../core'; +import {InterpolationConfig} from '../../ml_parser/interpolation_config'; import * as o from '../../output/output_ast'; import {ParseSourceSpan} from '../../parse_util'; import * as t from '../r3_ast'; @@ -184,7 +185,6 @@ export interface R3ComponentMetadata extends R3DirectiveMetadata { */ viewProviders: o.Expression|null; - /** * 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 @@ -197,6 +197,11 @@ export interface R3ComponentMetadata extends R3DirectiveMetadata { * (used by Closure Compiler's output of `goog.getMsg` for transition period) */ i18nUseExternalIds: boolean; + + /** + * Overrides the default interpolation start and end delimiters ({{ and }}) + */ + interpolation: InterpolationConfig; } /** diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 9aa5fd9330..30a122e232 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -14,6 +14,7 @@ import {ConstantPool, DefinitionKind} from '../../constant_pool'; import * as core from '../../core'; import {AST, ParsedEvent} from '../../expression_parser/ast'; import {LifecycleHooks} from '../../lifecycle_reflector'; +import {DEFAULT_INTERPOLATION_CONFIG} from '../../ml_parser/interpolation_config'; import * as o from '../../output/output_ast'; import {typeSourceSpan} from '../../parse_util'; import {CssSelector, SelectorMatcher} from '../../selector'; @@ -382,6 +383,7 @@ export function compileComponentFromRender2( styles: (summary.template && summary.template.styles) || EMPTY_ARRAY, encapsulation: (summary.template && summary.template.encapsulation) || core.ViewEncapsulation.Emulated, + interpolation: DEFAULT_INTERPOLATION_CONFIG, animations: null, viewProviders: component.viewProviders.length > 0 ? new o.WrappedNodeExpr(component.viewProviders) : null, diff --git a/packages/compiler/src/render3/view/i18n/meta.ts b/packages/compiler/src/render3/view/i18n/meta.ts index c277252737..1c2ec9b39a 100644 --- a/packages/compiler/src/render3/view/i18n/meta.ts +++ b/packages/compiler/src/render3/view/i18n/meta.ts @@ -10,7 +10,7 @@ import {decimalDigest} from '../../../i18n/digest'; import * as i18n from '../../../i18n/i18n_ast'; import {createI18nMessageFactory} from '../../../i18n/i18n_parser'; 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 {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. */ export class I18nMetaVisitor implements html.Visitor { - // i18n message generation factory - private _createI18nMessage = createI18nMessageFactory(DEFAULT_INTERPOLATION_CONFIG); + private _createI18nMessage: any; - 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( 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, // keeping only non-i18n related ones element.attrs = attrs; @@ -116,8 +120,12 @@ export class I18nMetaVisitor implements html.Visitor { 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( - html.visitAll(new I18nMetaVisitor({keepI18nAttrs: false}), htmlAstWithErrors.rootNodes), + html.visitAll( + new I18nMetaVisitor(interpolationConfig, /* keepI18nAttrs */ false), + htmlAstWithErrors.rootNodes), htmlAstWithErrors.errors); } \ No newline at end of file diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 580a3044a9..29ca6fbe0b 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -17,7 +17,7 @@ import * as i18n from '../../i18n/i18n_ast'; import * as html from '../../ml_parser/ast'; import {HtmlParser} from '../../ml_parser/html_parser'; 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 {mapLiteral} from '../../output/map_util'; 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 */ 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[]} { - const bindingParser = makeBindingParser(); + const {interpolationConfig, preserveWhitespaces} = options; + const bindingParser = makeBindingParser(interpolationConfig); 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) { return {errors: parseResult.errors, nodes: [], hasNgContent: false, ngContentSelectors: []}; @@ -1412,17 +1414,18 @@ export function parseTemplate( // before we run whitespace removal process, because existing i18n // extraction process (ng xi18n) relies on a raw content to generate // message ids - const i18nConfig = {keepI18nAttrs: !options.preserveWhitespaces}; - rootNodes = html.visitAll(new I18nMetaVisitor(i18nConfig), rootNodes); + rootNodes = + html.visitAll(new I18nMetaVisitor(interpolationConfig, !preserveWhitespaces), rootNodes); - if (!options.preserveWhitespaces) { + if (!preserveWhitespaces) { rootNodes = html.visitAll(new WhitespaceVisitor(), rootNodes); // run i18n meta visitor again in case we remove whitespaces, because // that might affect generated i18n message content. During this pass // i18n IDs generated at the first pass will be preserved, so we can mimic // 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} = @@ -1437,10 +1440,10 @@ export function parseTemplate( /** * Construct a `BindingParser` with a default configuration. */ -export function makeBindingParser(): BindingParser { +export function makeBindingParser( + interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): 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) { diff --git a/packages/compiler/src/template_parser/binding_parser.ts b/packages/compiler/src/template_parser/binding_parser.ts index 44d6c0951a..9c137277dd 100644 --- a/packages/compiler/src/template_parser/binding_parser.ts +++ b/packages/compiler/src/template_parser/binding_parser.ts @@ -45,6 +45,8 @@ export class BindingParser { } } + get interpolationConfig(): InterpolationConfig { return this._interpolationConfig; } + getUsedPipes(): CompilePipeSummary[] { return Array.from(this._usedPipes.values()); } createBoundHostProperties(dirMeta: CompileDirectiveSummary, sourceSpan: ParseSourceSpan): diff --git a/packages/core/src/render3/jit/compiler_facade_interface.ts b/packages/core/src/render3/jit/compiler_facade_interface.ts index 0c7b004d8f..5665359c73 100644 --- a/packages/core/src/render3/jit/compiler_facade_interface.ts +++ b/packages/core/src/render3/jit/compiler_facade_interface.ts @@ -132,6 +132,7 @@ export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade { styles: string[]; encapsulation: ViewEncapsulation; viewProviders: Provider[]|null; + interpolation?: [string, string]; } export type ViewEncapsulation = number; diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts index a08686f325..a51e64e709 100644 --- a/packages/core/src/render3/jit/directive.ts +++ b/packages/core/src/render3/jit/directive.ts @@ -61,6 +61,7 @@ export function compileComponent(type: Type, metadata: Component): void { directives: [], pipes: new Map(), encapsulation: metadata.encapsulation || ViewEncapsulation.Emulated, + interpolation: metadata.interpolation, viewProviders: metadata.viewProviders || null, }; ngComponentDef = compiler.compileComponent( diff --git a/packages/core/test/linker/integration_spec.ts b/packages/core/test/linker/integration_spec.ts index 4de1bee3fc..8d9555d515 100644 --- a/packages/core/test/linker/integration_spec.ts +++ b/packages/core/test/linker/integration_spec.ts @@ -1295,27 +1295,26 @@ function declareTests(config?: {useJit: boolean}) { expect(needsAttribute.fooAttribute).toBeNull(); }); - fixmeIvy('FW-723: Custom interpolation markers are not supported') && - it('should support custom interpolation', () => { - TestBed.configureTestingModule({ - declarations: [ - MyComp, ComponentWithCustomInterpolationA, ComponentWithCustomInterpolationB, - ComponentWithDefaultInterpolation - ] - }); - const template = `
{{ctxProp}}
+ it('should support custom interpolation', () => { + TestBed.configureTestingModule({ + declarations: [ + MyComp, ComponentWithCustomInterpolationA, ComponentWithCustomInterpolationB, + ComponentWithDefaultInterpolation + ] + }); + const template = `
{{ctxProp}}
`; - TestBed.overrideComponent(MyComp, {set: {template}}); - const fixture = TestBed.createComponent(MyComp); + TestBed.overrideComponent(MyComp, {set: {template}}); + const fixture = TestBed.createComponent(MyComp); - fixture.componentInstance.ctxProp = 'Default Interpolation'; + fixture.componentInstance.ctxProp = 'Default Interpolation'; - fixture.detectChanges(); - expect(fixture.nativeElement) - .toHaveText( - 'Default InterpolationCustom Interpolation ACustom Interpolation B (Default Interpolation)'); - }); + fixture.detectChanges(); + expect(fixture.nativeElement) + .toHaveText( + 'Default InterpolationCustom Interpolation ACustom Interpolation B (Default Interpolation)'); + }); }); describe('dependency injection', () => {