From 1e5812192e0ffa1d570f81137746069c3dfdd900 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 24 Nov 2020 18:08:16 +0100 Subject: [PATCH] fix(compiler): handle strings inside bindings that contain binding characters Currently the compiler treats something like `{{ '{{a}}' }}` as a nested binding and throws an error, because it doesn't account for quotes when it looks for binding characters. These changes add a bit of logic to skip over text inside quotes when parsing. Fixes #39601. --- .../compiler/src/expression_parser/parser.ts | 25 ++++++++++++++++--- .../template_parser/template_parser_spec.ts | 12 +++++++++ packages/core/test/acceptance/text_spec.ts | 14 +++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/packages/compiler/src/expression_parser/parser.ts b/packages/compiler/src/expression_parser/parser.ts index e87c2c3b61eee..f69b737380295 100644 --- a/packages/compiler/src/expression_parser/parser.ts +++ b/packages/compiler/src/expression_parser/parser.ts @@ -244,10 +244,10 @@ export class Parser { atInterpolation = true; } else { - // parse from starting {{ to ending }} + // parse from starting {{ to ending }} while ignoring content inside quotes. const fullStart = i; const exprStart = fullStart + interpStart.length; - const exprEnd = input.indexOf(interpEnd, exprStart); + const exprEnd = this._indexOfSkipQuoted(input, interpEnd, exprStart); if (exprEnd === -1) { // Could not find the end of the interpolation; do not parse an expression. // Instead we should extend the content on the last raw string. @@ -340,10 +340,29 @@ export class Parser { return errLocation.length; } + + /** Like `String.prototype.indexOf`, but skips content inside quotes. */ + private _indexOfSkipQuoted(input: string, value: string, start: number): number { + const valueLength = value.length; + let currentQuote: string|null = null; + for (let i = start; i < input.length; i++) { + const char = input[i]; + // Skip the characters inside quotes. Note that we + // only care about the outer-most quotes matching up. + if (isQuote(input.charCodeAt(i)) && (currentQuote === null || currentQuote === char)) { + currentQuote = currentQuote === null ? char : null; + } else if ( + currentQuote === null && char === value[0] && + (valueLength === 0 || input.substring(i, i + valueLength) === value)) { + return i; + } + } + return -1; + } } export class IvyParser extends Parser { - simpleExpressionChecker = IvySimpleExpressionChecker; // + simpleExpressionChecker = IvySimpleExpressionChecker; } /** Describes a stateful context an expression parser is in. */ diff --git a/packages/compiler/test/template_parser/template_parser_spec.ts b/packages/compiler/test/template_parser/template_parser_spec.ts index d43238c102f16..dd3f8877f1b94 100644 --- a/packages/compiler/test/template_parser/template_parser_spec.ts +++ b/packages/compiler/test/template_parser/template_parser_spec.ts @@ -540,6 +540,18 @@ describe('TemplateParser', () => { expect(humanizeTplAst(parse('{{a}}', []))).toEqual([[BoundTextAst, '{{ a }}']]); }); + it('should parse bound text nodes inside quotes', () => { + expect(humanizeTplAst(parse('"{{a}}"', []))).toEqual([[BoundTextAst, '"{{ a }}"']]); + }); + + it('should parse bound text nodes with interpolations inside quotes', () => { + expect(humanizeTplAst(parse('{{ "{{a}}" }}', []))).toEqual([[BoundTextAst, '{{ "{{a}}" }}']]); + }); + + it('should not parse bound text nodes with mismatching quotes', () => { + expect(humanizeTplAst(parse(`{{ "{{a}}' }}`, []))).toEqual([[TextAst, `{{ "{{a}}' }}`]]); + }); + it('should parse with custom interpolation config', inject([TemplateParser], (parser: TemplateParser) => { const component = CompileDirectiveMetadata.create({ diff --git a/packages/core/test/acceptance/text_spec.ts b/packages/core/test/acceptance/text_spec.ts index 7584c1e5e11f7..b91acf0e12f1e 100644 --- a/packages/core/test/acceptance/text_spec.ts +++ b/packages/core/test/acceptance/text_spec.ts @@ -129,4 +129,18 @@ describe('text instructions', () => { expect(div.innerHTML).toBe('function foo() { }'); }); + + it('should handle binding syntax used inside quoted text', () => { + @Component({ + template: `{{'Interpolations look like {{this}}'}}`, + }) + class App { + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toBe('Interpolations look like {{this}}'); + }); });