diff --git a/packages/compiler/test/ml_parser/lexer_spec.ts b/packages/compiler/test/ml_parser/lexer_spec.ts index 71690078e8e..3aaa0efd9e7 100644 --- a/packages/compiler/test/ml_parser/lexer_spec.ts +++ b/packages/compiler/test/ml_parser/lexer_spec.ts @@ -1438,421 +1438,6 @@ describe('HtmlLexer', () => { ]); }); }); - - describe('blocks', () => { - it('should parse a block without parameters', () => { - const expected = [ - [TokenType.BLOCK_OPEN_START, 'foo'], - [TokenType.BLOCK_OPEN_END], - [TokenType.TEXT, 'hello'], - [TokenType.BLOCK_CLOSE], - [TokenType.EOF], - ]; - - expect(tokenizeAndHumanizeParts('@foo {hello}')).toEqual(expected); - expect(tokenizeAndHumanizeParts('@foo () {hello}')).toEqual(expected); - expect(tokenizeAndHumanizeParts('@foo(){hello}')).toEqual(expected); - }); - - it('should parse a block with parameters', () => { - expect(tokenizeAndHumanizeParts('@for (item of items; track item.id) {hello}')).toEqual([ - [TokenType.BLOCK_OPEN_START, 'for'], - [TokenType.BLOCK_PARAMETER, 'item of items'], - [TokenType.BLOCK_PARAMETER, 'track item.id'], - [TokenType.BLOCK_OPEN_END], - [TokenType.TEXT, 'hello'], - [TokenType.BLOCK_CLOSE], - [TokenType.EOF], - ]); - }); - - it('should parse a block with a trailing semicolon after the parameters', () => { - expect(tokenizeAndHumanizeParts('@for (item of items;) {hello}')).toEqual([ - [TokenType.BLOCK_OPEN_START, 'for'], - [TokenType.BLOCK_PARAMETER, 'item of items'], - [TokenType.BLOCK_OPEN_END], - [TokenType.TEXT, 'hello'], - [TokenType.BLOCK_CLOSE], - [TokenType.EOF], - ]); - }); - - it('should parse a block with a space in its name', () => { - expect(tokenizeAndHumanizeParts('@else if {hello}')).toEqual([ - [TokenType.BLOCK_OPEN_START, 'else if'], - [TokenType.BLOCK_OPEN_END], - [TokenType.TEXT, 'hello'], - [TokenType.BLOCK_CLOSE], - [TokenType.EOF], - ]); - - expect(tokenizeAndHumanizeParts('@else if (foo !== 2) {hello}')).toEqual([ - [TokenType.BLOCK_OPEN_START, 'else if'], - [TokenType.BLOCK_PARAMETER, 'foo !== 2'], - [TokenType.BLOCK_OPEN_END], - [TokenType.TEXT, 'hello'], - [TokenType.BLOCK_CLOSE], - [TokenType.EOF], - ]); - }); - - it('should parse a block with an arbitrary amount of spaces around the parentheses', () => { - const expected = [ - [TokenType.BLOCK_OPEN_START, 'foo'], - [TokenType.BLOCK_PARAMETER, 'a'], - [TokenType.BLOCK_PARAMETER, 'b'], - [TokenType.BLOCK_PARAMETER, 'c'], - [TokenType.BLOCK_OPEN_END], - [TokenType.TEXT, 'hello'], - [TokenType.BLOCK_CLOSE], - [TokenType.EOF], - ]; - - expect(tokenizeAndHumanizeParts('@foo(a; b; c){hello}')).toEqual(expected); - expect(tokenizeAndHumanizeParts('@foo (a; b; c) {hello}')).toEqual(expected); - expect(tokenizeAndHumanizeParts('@foo(a; b; c) {hello}')).toEqual(expected); - expect(tokenizeAndHumanizeParts('@foo (a; b; c){hello}')).toEqual(expected); - }); - - it('should parse a block with multiple trailing semicolons', () => { - expect(tokenizeAndHumanizeParts('@for (item of items;;;;;) {hello}')).toEqual([ - [TokenType.BLOCK_OPEN_START, 'for'], - [TokenType.BLOCK_PARAMETER, 'item of items'], - [TokenType.BLOCK_OPEN_END], - [TokenType.TEXT, 'hello'], - [TokenType.BLOCK_CLOSE], - [TokenType.EOF], - ]); - }); - - it('should parse a block with trailing whitespace', () => { - expect(tokenizeAndHumanizeParts('@foo {hello}')).toEqual([ - [TokenType.BLOCK_OPEN_START, 'foo'], - [TokenType.BLOCK_OPEN_END], - [TokenType.TEXT, 'hello'], - [TokenType.BLOCK_CLOSE], - [TokenType.EOF], - ]); - }); - - it('should parse a block with no trailing semicolon', () => { - expect(tokenizeAndHumanizeParts('@for (item of items){hello}')).toEqual([ - [TokenType.BLOCK_OPEN_START, 'for'], - [TokenType.BLOCK_PARAMETER, 'item of items'], - [TokenType.BLOCK_OPEN_END], - [TokenType.TEXT, 'hello'], - [TokenType.BLOCK_CLOSE], - [TokenType.EOF], - ]); - }); - - it('should handle semicolons, braces and parentheses used in a block parameter', () => { - const input = `@foo (a === ";"; b === ')'; c === "("; d === '}'; e === "{") {hello}`; - expect(tokenizeAndHumanizeParts(input)).toEqual([ - [TokenType.BLOCK_OPEN_START, 'foo'], - [TokenType.BLOCK_PARAMETER, `a === ";"`], - [TokenType.BLOCK_PARAMETER, `b === ')'`], - [TokenType.BLOCK_PARAMETER, `c === "("`], - [TokenType.BLOCK_PARAMETER, `d === '}'`], - [TokenType.BLOCK_PARAMETER, `e === "{"`], - [TokenType.BLOCK_OPEN_END], - [TokenType.TEXT, 'hello'], - [TokenType.BLOCK_CLOSE], - [TokenType.EOF], - ]); - }); - - it('should handle object literals and function calls in block parameters', () => { - expect( - tokenizeAndHumanizeParts( - `@foo (on a({a: 1, b: 2}, false, {c: 3}); when b({d: 4})) {hello}`, - ), - ).toEqual([ - [TokenType.BLOCK_OPEN_START, 'foo'], - [TokenType.BLOCK_PARAMETER, 'on a({a: 1, b: 2}, false, {c: 3})'], - [TokenType.BLOCK_PARAMETER, 'when b({d: 4})'], - [TokenType.BLOCK_OPEN_END], - [TokenType.TEXT, 'hello'], - [TokenType.BLOCK_CLOSE], - [TokenType.EOF], - ]); - }); - - it('should parse block with unclosed parameters', () => { - expect(tokenizeAndHumanizeParts(`@foo (a === b {hello}`)).toEqual([ - [TokenType.INCOMPLETE_BLOCK_OPEN, 'foo'], - [TokenType.BLOCK_PARAMETER, 'a === b {hello}'], - [TokenType.EOF], - ]); - }); - - it('should parse block with stray parentheses in the parameter position', () => { - expect(tokenizeAndHumanizeParts(`@foo a === b) {hello}`)).toEqual([ - [TokenType.INCOMPLETE_BLOCK_OPEN, 'foo a'], - [TokenType.TEXT, '=== b) {hello'], - [TokenType.BLOCK_CLOSE], - [TokenType.EOF], - ]); - }); - - it('should parse @ as an incomplete block', () => { - expect(tokenizeAndHumanizeParts(`@`)).toEqual([ - [TokenType.INCOMPLETE_BLOCK_OPEN, ''], - [TokenType.EOF], - ]); - }); - - it('should parse space followed by @ as an incomplete block', () => { - expect(tokenizeAndHumanizeParts(` @`)).toEqual([ - [TokenType.TEXT, ' '], - [TokenType.INCOMPLETE_BLOCK_OPEN, ''], - [TokenType.EOF], - ]); - }); - - it('should parse @ followed by space as an incomplete block', () => { - expect(tokenizeAndHumanizeParts(`@ `)).toEqual([ - [TokenType.INCOMPLETE_BLOCK_OPEN, ''], - [TokenType.TEXT, ' '], - [TokenType.EOF], - ]); - }); - - it('should parse @ followed by newline and text as an incomplete block', () => { - expect(tokenizeAndHumanizeParts(`@\nfoo`)).toEqual([ - [TokenType.INCOMPLETE_BLOCK_OPEN, ''], - [TokenType.TEXT, '\nfoo'], - [TokenType.EOF], - ]); - }); - - it('should parse incomplete block with no name', () => { - expect(tokenizeAndHumanizeParts(`foo bar @ baz clink`)).toEqual([ - [TokenType.TEXT, 'foo bar '], - [TokenType.INCOMPLETE_BLOCK_OPEN, ''], - [TokenType.TEXT, ' baz clink'], - [TokenType.EOF], - ]); - }); - - it('should parse incomplete block with space, then name', () => { - expect(tokenizeAndHumanizeParts(`@ if`)).toEqual([ - [TokenType.INCOMPLETE_BLOCK_OPEN, ''], - [TokenType.TEXT, ' if'], - [TokenType.EOF], - ]); - }); - - it('should report invalid quotes in a parameter', () => { - expect(tokenizeAndHumanizeErrors(`@foo (a === ") {hello}`)).toEqual([ - ['Unexpected character "EOF"', '0:22'], - ]); - - expect(tokenizeAndHumanizeErrors(`@foo (a === "hi') {hello}`)).toEqual([ - ['Unexpected character "EOF"', '0:25'], - ]); - }); - - it('should report unclosed object literal inside a parameter', () => { - expect(tokenizeAndHumanizeParts(`@foo ({invalid: true) hello}`)).toEqual([ - [TokenType.INCOMPLETE_BLOCK_OPEN, 'foo'], - [TokenType.BLOCK_PARAMETER, '{invalid: true'], - [TokenType.TEXT, 'hello'], - [TokenType.BLOCK_CLOSE], - [TokenType.EOF], - ]); - }); - - it('should handle a semicolon used in a nested string inside a block parameter', () => { - expect(tokenizeAndHumanizeParts(`@if (condition === "';'") {hello}`)).toEqual([ - [TokenType.BLOCK_OPEN_START, 'if'], - [TokenType.BLOCK_PARAMETER, `condition === "';'"`], - [TokenType.BLOCK_OPEN_END], - [TokenType.TEXT, 'hello'], - [TokenType.BLOCK_CLOSE], - [TokenType.EOF], - ]); - }); - - it('should handle a semicolon next to an escaped quote used in a block parameter', () => { - expect(tokenizeAndHumanizeParts('@if (condition === "\\";") {hello}')).toEqual([ - [TokenType.BLOCK_OPEN_START, 'if'], - [TokenType.BLOCK_PARAMETER, 'condition === "\\";"'], - [TokenType.BLOCK_OPEN_END], - [TokenType.TEXT, 'hello'], - [TokenType.BLOCK_CLOSE], - [TokenType.EOF], - ]); - }); - - it('should parse mixed text and html content in a block', () => { - expect(tokenizeAndHumanizeParts('@if (a === 1) {foo bar baz}')).toEqual([ - [TokenType.BLOCK_OPEN_START, 'if'], - [TokenType.BLOCK_PARAMETER, 'a === 1'], - [TokenType.BLOCK_OPEN_END], - [TokenType.TEXT, 'foo '], - [TokenType.TAG_OPEN_START, '', 'b'], - [TokenType.TAG_OPEN_END], - [TokenType.TEXT, 'bar'], - [TokenType.TAG_CLOSE, '', 'b'], - [TokenType.TEXT, ' baz'], - [TokenType.BLOCK_CLOSE], - [TokenType.EOF], - ]); - }); - - it('should parse HTML tags with attributes containing curly braces inside blocks', () => { - expect(tokenizeAndHumanizeParts('@if (a === 1) {
}')).toEqual([ - [TokenType.BLOCK_OPEN_START, 'if'], - [TokenType.BLOCK_PARAMETER, 'a === 1'], - [TokenType.BLOCK_OPEN_END], - [TokenType.TAG_OPEN_START, '', 'div'], - [TokenType.ATTR_NAME, '', 'a'], - [TokenType.ATTR_QUOTE, '"'], - [TokenType.ATTR_VALUE_TEXT, '}'], - [TokenType.ATTR_QUOTE, '"'], - [TokenType.ATTR_NAME, '', 'b'], - [TokenType.ATTR_QUOTE, '"'], - [TokenType.ATTR_VALUE_TEXT, '{'], - [TokenType.ATTR_QUOTE, '"'], - [TokenType.TAG_OPEN_END], - [TokenType.TAG_CLOSE, '', 'div'], - [TokenType.BLOCK_CLOSE], - [TokenType.EOF], - ]); - }); - - it('should parse HTML tags with attribute containing block syntax', () => { - expect(tokenizeAndHumanizeParts('')).toEqual([ - [TokenType.TAG_OPEN_START, '', 'div'], - [TokenType.ATTR_NAME, '', 'a'], - [TokenType.ATTR_QUOTE, '"'], - [TokenType.ATTR_VALUE_TEXT, '@if (foo) {}'], - [TokenType.ATTR_QUOTE, '"'], - [TokenType.TAG_OPEN_END], - [TokenType.TAG_CLOSE, '', 'div'], - [TokenType.EOF], - ]); - }); - - it('should parse nested blocks', () => { - expect( - tokenizeAndHumanizeParts( - '@if (a) {' + - 'hello a' + - '@if {' + - 'hello unnamed' + - '@if (b) {' + - 'hello b' + - '@if (c) {' + - 'hello c' + - '}' + - '}' + - '}' + - '}', - ), - ).toEqual([ - [TokenType.BLOCK_OPEN_START, 'if'], - [TokenType.BLOCK_PARAMETER, 'a'], - [TokenType.BLOCK_OPEN_END], - [TokenType.TEXT, 'hello a'], - [TokenType.BLOCK_OPEN_START, 'if'], - [TokenType.BLOCK_OPEN_END], - [TokenType.TEXT, 'hello unnamed'], - [TokenType.BLOCK_OPEN_START, 'if'], - [TokenType.BLOCK_PARAMETER, 'b'], - [TokenType.BLOCK_OPEN_END], - [TokenType.TEXT, 'hello b'], - [TokenType.BLOCK_OPEN_START, 'if'], - [TokenType.BLOCK_PARAMETER, 'c'], - [TokenType.BLOCK_OPEN_END], - [TokenType.TEXT, 'hello c'], - [TokenType.BLOCK_CLOSE], - [TokenType.BLOCK_CLOSE], - [TokenType.BLOCK_CLOSE], - [TokenType.BLOCK_CLOSE], - [TokenType.EOF], - ]); - }); - - it('should parse a block containing an expansion', () => { - const result = tokenizeAndHumanizeParts( - '@foo {{one.two, three, =4 {four} =5 {five} foo {bar} }}', - {tokenizeExpansionForms: true}, - ); - - expect(result).toEqual([ - [TokenType.BLOCK_OPEN_START, 'foo'], - [TokenType.BLOCK_OPEN_END], - [TokenType.EXPANSION_FORM_START], - [TokenType.RAW_TEXT, 'one.two'], - [TokenType.RAW_TEXT, 'three'], - [TokenType.EXPANSION_CASE_VALUE, '=4'], - [TokenType.EXPANSION_CASE_EXP_START], - [TokenType.TEXT, 'four'], - [TokenType.EXPANSION_CASE_EXP_END], - [TokenType.EXPANSION_CASE_VALUE, '=5'], - [TokenType.EXPANSION_CASE_EXP_START], - [TokenType.TEXT, 'five'], - [TokenType.EXPANSION_CASE_EXP_END], - [TokenType.EXPANSION_CASE_VALUE, 'foo'], - [TokenType.EXPANSION_CASE_EXP_START], - [TokenType.TEXT, 'bar'], - [TokenType.EXPANSION_CASE_EXP_END], - [TokenType.EXPANSION_FORM_END], - [TokenType.BLOCK_CLOSE], - [TokenType.EOF], - ]); - }); - - it('should parse a block containing an interpolation', () => { - expect(tokenizeAndHumanizeParts('@foo {{{message}}}')).toEqual([ - [TokenType.BLOCK_OPEN_START, 'foo'], - [TokenType.BLOCK_OPEN_END], - [TokenType.TEXT, ''], - [TokenType.INTERPOLATION, '{{', 'message', '}}'], - [TokenType.TEXT, ''], - [TokenType.BLOCK_CLOSE], - [TokenType.EOF], - ]); - }); - - it('should parse an incomplete block start without parameters with surrounding text', () => { - expect(tokenizeAndHumanizeParts('My email frodo@baggins.com')).toEqual([ - [TokenType.TEXT, 'My email frodo'], - [TokenType.INCOMPLETE_BLOCK_OPEN, 'baggins'], - [TokenType.TEXT, '.com'], - [TokenType.EOF], - ]); - }); - - it('should parse an incomplete block start at the end of the input', () => { - expect(tokenizeAndHumanizeParts('My username is @frodo')).toEqual([ - [TokenType.TEXT, 'My username is '], - [TokenType.INCOMPLETE_BLOCK_OPEN, 'frodo'], - [TokenType.EOF], - ]); - }); - - it('should parse an incomplete block start with parentheses but without params', () => { - expect(tokenizeAndHumanizeParts('Use the @Input() decorator')).toEqual([ - [TokenType.TEXT, 'Use the '], - [TokenType.INCOMPLETE_BLOCK_OPEN, 'Input'], - [TokenType.TEXT, 'decorator'], - [TokenType.EOF], - ]); - }); - - it('should parse an incomplete block start with parentheses and params', () => { - expect(tokenizeAndHumanizeParts('Use @Input({alias: "foo"}) to alias the input')).toEqual([ - [TokenType.TEXT, 'Use '], - [TokenType.INCOMPLETE_BLOCK_OPEN, 'Input'], - [TokenType.BLOCK_PARAMETER, '{alias: "foo"}'], - [TokenType.TEXT, 'to alias the input'], - [TokenType.EOF], - ]); - }); - }); }); describe('@let declarations', () => { @@ -4043,6 +3628,54 @@ describe('HtmlLexer', () => { [TokenType.EOF], ]); }); + + it('should parse @ as an incomplete block', () => { + expect(tokenizeAndHumanizeParts(`@`)).toEqual([ + [TokenType.INCOMPLETE_BLOCK_OPEN, ''], + [TokenType.EOF], + ]); + }); + + it('should parse space followed by @ as an incomplete block', () => { + expect(tokenizeAndHumanizeParts(` @`)).toEqual([ + [TokenType.TEXT, ' '], + [TokenType.INCOMPLETE_BLOCK_OPEN, ''], + [TokenType.EOF], + ]); + }); + + it('should parse @ followed by space as an incomplete block', () => { + expect(tokenizeAndHumanizeParts(`@ `)).toEqual([ + [TokenType.INCOMPLETE_BLOCK_OPEN, ''], + [TokenType.TEXT, ' '], + [TokenType.EOF], + ]); + }); + + it('should parse @ followed by newline and text as an incomplete block', () => { + expect(tokenizeAndHumanizeParts(`@\nfoo`)).toEqual([ + [TokenType.INCOMPLETE_BLOCK_OPEN, ''], + [TokenType.TEXT, '\nfoo'], + [TokenType.EOF], + ]); + }); + + it('should parse incomplete block with no name', () => { + expect(tokenizeAndHumanizeParts(`foo bar @ baz clink`)).toEqual([ + [TokenType.TEXT, 'foo bar '], + [TokenType.INCOMPLETE_BLOCK_OPEN, ''], + [TokenType.TEXT, ' baz clink'], + [TokenType.EOF], + ]); + }); + + it('should parse incomplete block with space, then name', () => { + expect(tokenizeAndHumanizeParts(`@ if`)).toEqual([ + [TokenType.INCOMPLETE_BLOCK_OPEN, ''], + [TokenType.TEXT, ' if'], + [TokenType.EOF], + ]); + }); }); });