diff --git a/packages/compiler/src/ml_parser/lexer.ts b/packages/compiler/src/ml_parser/lexer.ts index ce170b11961..d5d3b670271 100644 --- a/packages/compiler/src/ml_parser/lexer.ts +++ b/packages/compiler/src/ml_parser/lexer.ts @@ -263,11 +263,11 @@ class _Tokenizer { this._beginToken(TokenType.BLOCK_PARAMETER); const start = this._cursor.clone(); let inQuote: number|null = null; + let openBraces = 0; // Consume the parameter until the next semicolon or brace. // Note that we skip over semicolons/braces inside of strings. - while ((this._cursor.peek() !== chars.$SEMICOLON && this._cursor.peek() !== chars.$RBRACE && - this._cursor.peek() !== chars.$EOF) || + while ((this._cursor.peek() !== chars.$SEMICOLON && this._cursor.peek() !== chars.$EOF) || inQuote !== null) { const char = this._cursor.peek(); @@ -278,6 +278,14 @@ class _Tokenizer { inQuote = null; } else if (inQuote === null && chars.isQuote(char)) { inQuote = char; + } else if (char === chars.$LBRACE && inQuote === null) { + openBraces++; + } else if (char === chars.$RBRACE && inQuote === null) { + if (openBraces === 0) { + break; + } else if (openBraces > 0) { + openBraces--; + } } this._cursor.advance(); diff --git a/packages/compiler/test/ml_parser/lexer_spec.ts b/packages/compiler/test/ml_parser/lexer_spec.ts index ea5a1a3c548..f039c907638 100644 --- a/packages/compiler/test/ml_parser/lexer_spec.ts +++ b/packages/compiler/test/ml_parser/lexer_spec.ts @@ -1817,8 +1817,10 @@ import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_u describe('blocks', () => { // TODO(crisbeto): temporary utility while blocks are disabled by default. - function tokenizeBlock(input: string, options: TokenizeOptions = {}): any[] { - return tokenizeAndHumanizeParts(input, {tokenizeBlocks: true, ...options}); + const options: Readonly = {tokenizeBlocks: true}; + + function tokenizeBlock(input: string, additionalOptions: TokenizeOptions = {}): any[] { + return tokenizeAndHumanizeParts(input, {...options, ...additionalOptions}); } it('should parse a block group', () => { @@ -1887,10 +1889,11 @@ import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_u }); it('should handle semicolons and braces used in a block group parameter', () => { - expect(tokenizeBlock(`{#foo a === ";"; b === '}'}hello{/foo}`)).toEqual([ + expect(tokenizeBlock(`{#foo a === ";"; b === '}'; c === "{"}hello{/foo}`)).toEqual([ [TokenType.BLOCK_GROUP_OPEN_START, 'foo'], [TokenType.BLOCK_PARAMETER, `a === ";"`], [TokenType.BLOCK_PARAMETER, `b === '}'`], + [TokenType.BLOCK_PARAMETER, `c === "{"`], [TokenType.BLOCK_GROUP_OPEN_END], [TokenType.TEXT, 'hello'], [TokenType.BLOCK_GROUP_CLOSE, 'foo'], @@ -1898,9 +1901,20 @@ import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_u ]); }); - it('should throw for invalid quotes in a parameter', () => { - const options = {tokenizeBlocks: true}; + it('should handle object literals in block group parameters', () => { + expect(tokenizeBlock(`{#foo on a({a: 1, b: 2}, false, {c: 3}); when b({d: 4})}hello{/foo}`)) + .toEqual([ + [TokenType.BLOCK_GROUP_OPEN_START, 'foo'], + [TokenType.BLOCK_PARAMETER, 'on a({a: 1, b: 2}, false, {c: 3})'], + [TokenType.BLOCK_PARAMETER, 'when b({d: 4})'], + [TokenType.BLOCK_GROUP_OPEN_END], + [TokenType.TEXT, 'hello'], + [TokenType.BLOCK_GROUP_CLOSE, 'foo'], + [TokenType.EOF], + ]); + }); + it('should report invalid quotes in a parameter', () => { expect(tokenizeAndHumanizeErrors(`{#foo a === "}hello{/foo}`, options)).toEqual([ [TokenType.BLOCK_PARAMETER, 'Unexpected character "EOF"', '0:25'] ]); @@ -1910,6 +1924,18 @@ import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_u ]); }); + it('should report unclosed block open tag containing an objet literal', () => { + expect(tokenizeAndHumanizeErrors(`{#foo {invalid: true}hello{/foo}`, options)).toEqual([ + [TokenType.BLOCK_GROUP_OPEN_END, 'Unexpected character "EOF"', '0:32'], + ]); + }); + + it('should report unclosed object literal in block open tag', () => { + expect(tokenizeAndHumanizeErrors(`{#foo {hello{/foo}`, options)).toEqual([ + [TokenType.BLOCK_GROUP_OPEN_END, 'Unexpected character "EOF"', '0:18'], + ]); + }); + it('should handle a semicolon used in a nested string inside a block group parameter', () => { expect(tokenizeBlock(`{#if condition === "';'"}hello{/if}`)).toEqual([ [TokenType.BLOCK_GROUP_OPEN_START, 'if'],