diff --git a/packages/compiler/src/expression_parser/parser.ts b/packages/compiler/src/expression_parser/parser.ts index 4a799287fca5c..feb3c3c3c78b9 100644 --- a/packages/compiler/src/expression_parser/parser.ts +++ b/packages/compiler/src/expression_parser/parser.ts @@ -247,7 +247,7 @@ export class Parser { // parse from starting {{ to ending }} while ignoring content inside quotes. const fullStart = i; const exprStart = fullStart + interpStart.length; - const exprEnd = this._indexOfSkipQuoted(input, interpEnd, exprStart); + const exprEnd = this._getExpressiondEndIndex(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. @@ -341,20 +341,31 @@ 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; + /** + * Finds the index of the end of an interpolation expression + * while ignoring comments and quoted content. + */ + private _getExpressiondEndIndex(input: string, expressionEnd: string, start: number): number { let currentQuote: string|null = null; + let escapeCount = 0; 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] !== '\\') { + escapeCount % 2 === 0) { currentQuote = currentQuote === null ? char : null; - } else if (currentQuote === null && char === value[0] && input.startsWith(value, i)) { - return i; + } else if (currentQuote === null) { + if (input.startsWith(expressionEnd, i)) { + return i; + } + // Nothing else in the expression matters after we've + // hit a comment so look directly for the end token. + if (input.startsWith('//', i)) { + return input.indexOf(expressionEnd, i); + } } + escapeCount = char === '\\' ? escapeCount + 1 : 0; } return -1; } diff --git a/packages/compiler/test/expression_parser/parser_spec.ts b/packages/compiler/test/expression_parser/parser_spec.ts index 2ccd8f5a633df..2acde1d89fa22 100644 --- a/packages/compiler/test/expression_parser/parser_spec.ts +++ b/packages/compiler/test/expression_parser/parser_spec.ts @@ -859,6 +859,12 @@ describe('parser', () => { checkInterpolation(`{{'It\\'s }} just Angular'}}`, `{{ "It's }} just Angular" }}`); }); + it('should parse interpolation with escaped backslashes', () => { + checkInterpolation(`{{foo.split('\\\\')}}`, `{{ foo.split("\\") }}`); + checkInterpolation(`{{foo.split('\\\\\\\\')}}`, `{{ foo.split("\\\\") }}`); + checkInterpolation(`{{foo.split('\\\\\\\\\\\\')}}`, `{{ foo.split("\\\\\\") }}`); + }); + it('should not parse interpolation with mismatching quotes', () => { expect(parseInterpolation(`{{ "{{a}}' }}`)).toBeNull(); }); @@ -920,6 +926,10 @@ describe('parser', () => { it('should retain // in nested, unterminated strings', () => { checkInterpolation(`{{ "a\'b\`" //comment}}`, `{{ "a\'b\`" }}`); }); + + it('should ignore quotes inside a comment', () => { + checkInterpolation(`"{{name // " }}"`, `"{{ name }}"`); + }); }); }); @@ -1100,8 +1110,11 @@ function parseSimpleBindingIvy( } function checkInterpolation(exp: string, expected?: string) { - const ast = parseInterpolation(exp)!; + const ast = parseInterpolation(exp); if (expected == null) expected = exp; + if (ast === null) { + throw Error(`Failed to parse expression "${exp}"`); + } expect(unparse(ast)).toEqual(expected); validate(ast); } diff --git a/packages/compiler/test/template_parser/template_parser_spec.ts b/packages/compiler/test/template_parser/template_parser_spec.ts index 245ae8e93bba0..69a0c3e75441b 100644 --- a/packages/compiler/test/template_parser/template_parser_spec.ts +++ b/packages/compiler/test/template_parser/template_parser_spec.ts @@ -570,6 +570,24 @@ describe('TemplateParser', () => { expect(humanizeTplAst(parse(`{{ "{{a}}' }}`, []))).toEqual([[TextAst, `{{ "{{a}}' }}`]]); }); + it('should parse interpolation with escaped backslashes', () => { + expect(humanizeTplAst(parse(`{{foo.split('\\\\')}}`, []))).toEqual([ + [BoundTextAst, `{{ foo.split("\\") }}`] + ]); + expect(humanizeTplAst(parse(`{{foo.split('\\\\\\\\')}}`, []))).toEqual([ + [BoundTextAst, `{{ foo.split("\\\\") }}`] + ]); + expect(humanizeTplAst(parse(`{{foo.split('\\\\\\\\\\\\')}}`, []))).toEqual([ + [BoundTextAst, `{{ foo.split("\\\\\\") }}`] + ]); + }); + + it('should ignore quotes inside a comment', () => { + expect(humanizeTplAst(parse(`"{{name // " }}"`, []))).toEqual([ + [BoundTextAst, `"{{ name }}"`] + ]); + }); + it('should parse with custom interpolation config', inject([TemplateParser], (parser: TemplateParser) => { const component = CompileDirectiveMetadata.create({