diff --git a/packages/compiler/src/expression_parser/parser.ts b/packages/compiler/src/expression_parser/parser.ts index 863c8609934be..8d93b5c54fc67 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,30 @@ 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 and we need to account for escape characters. + if (isQuote(input.charCodeAt(i)) && (currentQuote === null || currentQuote === char) && + input[i - 1] !== '\\') { + 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..efd999205b242 100644 --- a/packages/compiler/test/template_parser/template_parser_spec.ts +++ b/packages/compiler/test/template_parser/template_parser_spec.ts @@ -540,6 +540,24 @@ 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 parse bound text nodes with escaped quotes', () => { + expect(humanizeTplAst(parse(`{{'It\\'s just Angular'}}`, []))).toEqual([ + [BoundTextAst, `{{ "It's just Angular" }}`] + ]); + }); + + 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 4d25f93333552..952006c5a9a2a 100644 --- a/packages/core/test/acceptance/text_spec.ts +++ b/packages/core/test/acceptance/text_spec.ts @@ -171,4 +171,18 @@ describe('text instructions', () => { // `Symbol(hello)_p.sc8s398cplk`, whereas the native one is `Symbol(hello)`. expect(fixture.nativeElement.textContent).toContain('Symbol(hello)'); }); + + 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}}'); + }); });