/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import * as html from '../../src/ml_parser/ast'; import {HtmlParser, ParseTreeResult, TreeError} from '../../src/ml_parser/html_parser'; import {TokenType} from '../../src/ml_parser/lexer'; import {ParseError} from '../../src/parse_util'; import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn, humanizeNodes} from './ast_spec_utils'; { describe('HtmlParser', () => { let parser: HtmlParser; beforeEach(() => { parser = new HtmlParser(); }); describe('parse', () => { describe('text nodes', () => { it('should parse root level text nodes', () => { expect(humanizeDom(parser.parse('a', 'TestComp'))).toEqual([[html.Text, 'a', 0]]); }); it('should parse text nodes inside regular elements', () => { expect(humanizeDom(parser.parse('
a
', 'TestComp'))).toEqual([ [html.Element, 'div', 0], [html.Text, 'a', 1] ]); }); it('should parse text nodes inside elements', () => { expect(humanizeDom(parser.parse('a', 'TestComp'))).toEqual([ [html.Element, 'ng-template', 0], [html.Text, 'a', 1] ]); }); it('should parse CDATA', () => { expect(humanizeDom(parser.parse('', 'TestComp'))).toEqual([ [html.Text, 'text', 0] ]); }); it('should normalize line endings within CDATA', () => { const parsed = parser.parse('', 'TestComp'); expect(humanizeDom(parsed)).toEqual([ [html.Text, ' line 1 \n line 2 ', 0], ]); expect(parsed.errors).toEqual([]); }); }); describe('elements', () => { it('should parse root level elements', () => { expect(humanizeDom(parser.parse('
', 'TestComp'))).toEqual([ [html.Element, 'div', 0] ]); }); it('should parse elements inside of regular elements', () => { expect(humanizeDom(parser.parse('
', 'TestComp'))).toEqual([ [html.Element, 'div', 0], [html.Element, 'span', 1] ]); }); it('should parse elements inside elements', () => { expect(humanizeDom(parser.parse('', 'TestComp'))) .toEqual([[html.Element, 'ng-template', 0], [html.Element, 'span', 1]]); }); it('should support void elements', () => { expect(humanizeDom(parser.parse('', 'TestComp'))) .toEqual([ [html.Element, 'link', 0], [html.Attribute, 'rel', 'author license'], [html.Attribute, 'href', '/about'], ]); }); it('should not error on void elements from HTML5 spec', () => { // https://html.spec.whatwg.org/multipage/syntax.html#syntax-elements without: // - it can be present in head only // - it can be present in head only // - obsolete // - obsolete ['', '

', '', '
', '

', '
', '
', '/', '', '', '

', ].forEach((html) => { expect(parser.parse(html, 'TestComp').errors).toEqual([]); }); }); it('should close void elements on text nodes', () => { expect(humanizeDom(parser.parse('

before
after

', 'TestComp'))).toEqual([ [html.Element, 'p', 0], [html.Text, 'before', 1], [html.Element, 'br', 1], [html.Text, 'after', 1], ]); }); it('should support optional end tags', () => { expect(humanizeDom(parser.parse('

1

2

', 'TestComp'))).toEqual([ [html.Element, 'div', 0], [html.Element, 'p', 1], [html.Text, '1', 2], [html.Element, 'p', 1], [html.Text, '2', 2], ]); }); it('should support nested elements', () => { expect(humanizeDom(parser.parse('
', 'TestComp'))) .toEqual([ [html.Element, 'ul', 0], [html.Element, 'li', 1], [html.Element, 'ul', 2], [html.Element, 'li', 3], ]); }); /** * Certain elements (like or ) require parent elements of a certain type (ex. * can only be inside / ). The Angular HTML parser doesn't validate those * HTML compliancy rules as "problematic" elements can be projected - in such case HTML (as * written in an Angular template) might be "invalid" (spec-wise) but the resulting DOM will * still be correct. */ it('should not wraps elements in a required parent', () => { expect(humanizeDom(parser.parse('
', 'TestComp'))).toEqual([ [html.Element, 'div', 0], [html.Element, 'tr', 1], ]); }); it('should support explicit namespace', () => { expect(humanizeDom(parser.parse('', 'TestComp'))).toEqual([ [html.Element, ':myns:div', 0] ]); }); it('should support implicit namespace', () => { expect(humanizeDom(parser.parse('', 'TestComp'))).toEqual([ [html.Element, ':svg:svg', 0] ]); }); it('should propagate the namespace', () => { expect(humanizeDom(parser.parse('

', 'TestComp'))).toEqual([ [html.Element, ':myns:div', 0], [html.Element, ':myns:p', 1], ]); }); it('should match closing tags case sensitive', () => { const errors = parser.parse('

', 'TestComp').errors; expect(errors.length).toEqual(2); expect(humanizeErrors(errors)).toEqual([ [ 'p', 'Unexpected closing tag "p". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags', '0:8' ], [ 'dIv', 'Unexpected closing tag "dIv". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags', '0:12' ], ]); }); it('should support self closing void elements', () => { expect(humanizeDom(parser.parse('', 'TestComp'))).toEqual([ [html.Element, 'input', 0] ]); }); it('should support self closing foreign elements', () => { expect(humanizeDom(parser.parse('', 'TestComp'))).toEqual([ [html.Element, ':math:math', 0] ]); }); it('should ignore LF immediately after textarea, pre and listing', () => { expect(humanizeDom(parser.parse( '

\n

\n\n
\n\n', 'TestComp'))) .toEqual([ [html.Element, 'p', 0], [html.Text, '\n', 1], [html.Element, 'textarea', 0], [html.Element, 'pre', 0], [html.Text, '\n', 1], [html.Element, 'listing', 0], [html.Text, '\n', 1], ]); }); it('should normalize line endings in text', () => { let parsed: ParseTreeResult; parsed = parser.parse(' line 1 \r\n line 2 ', 'TestComp'); expect(humanizeDom(parsed)).toEqual([ [html.Element, 'title', 0], [html.Text, ' line 1 \n line 2 ', 1], ]); expect(parsed.errors).toEqual([]); parsed = parser.parse('', 'TestComp'); expect(humanizeDom(parsed)).toEqual([ [html.Element, 'script', 0], [html.Text, ' line 1 \n line 2 ', 1], ]); expect(parsed.errors).toEqual([]); parsed = parser.parse('
line 1 \r\n line 2
', 'TestComp'); expect(humanizeDom(parsed)).toEqual([ [html.Element, 'div', 0], [html.Text, ' line 1 \n line 2 ', 1], ]); expect(parsed.errors).toEqual([]); parsed = parser.parse(' line 1 \r\n line 2 ', 'TestComp'); expect(humanizeDom(parsed)).toEqual([ [html.Element, 'span', 0], [html.Text, ' line 1 \n line 2 ', 1], ]); expect(parsed.errors).toEqual([]); }); }); describe('attributes', () => { it('should parse attributes on regular elements case sensitive', () => { expect(humanizeDom(parser.parse('
', 'TestComp'))).toEqual([ [html.Element, 'div', 0], [html.Attribute, 'kEy', 'v'], [html.Attribute, 'key2', 'v2'], ]); }); it('should normalize line endings within attribute values', () => { const result = parser.parse('
', 'TestComp'); expect(humanizeDom(result)).toEqual([ [html.Element, 'div', 0], [html.Attribute, 'key', ' \n line 1 \n line 2 '], ]); expect(result.errors).toEqual([]); }); it('should parse attributes without values', () => { expect(humanizeDom(parser.parse('
', 'TestComp'))).toEqual([ [html.Element, 'div', 0], [html.Attribute, 'k', ''], ]); }); it('should parse attributes on svg elements case sensitive', () => { expect(humanizeDom(parser.parse('', 'TestComp'))).toEqual([ [html.Element, ':svg:svg', 0], [html.Attribute, 'viewBox', '0'], ]); }); it('should parse attributes on elements', () => { expect(humanizeDom(parser.parse('', 'TestComp'))) .toEqual([ [html.Element, 'ng-template', 0], [html.Attribute, 'k', 'v'], ]); }); it('should support namespace', () => { expect(humanizeDom(parser.parse('', 'TestComp'))).toEqual([ [html.Element, ':svg:use', 0], [html.Attribute, ':xlink:href', 'Port'], ]); }); }); describe('comments', () => { it('should preserve comments', () => { expect(humanizeDom(parser.parse('
', 'TestComp'))).toEqual([ [html.Comment, 'comment', 0], [html.Element, 'div', 0], ]); }); it('should normalize line endings within comments', () => { expect(humanizeDom(parser.parse('', 'TestComp'))).toEqual([ [html.Comment, 'line 1 \n line 2', 0], ]); }); }); describe('expansion forms', () => { it('should parse out expansion forms', () => { const parsed = parser.parse( `
before{messages.length, plural, =0 {You have no messages} =1 {One {{message}}}}after
`, 'TestComp', {tokenizeExpansionForms: true}); expect(humanizeDom(parsed)).toEqual([ [html.Element, 'div', 0], [html.Text, 'before', 1], [html.Expansion, 'messages.length', 'plural', 1], [html.ExpansionCase, '=0', 2], [html.ExpansionCase, '=1', 2], [html.Text, 'after', 1], ]); const cases = (parsed.rootNodes[0]).children[1].cases; expect(humanizeDom(new ParseTreeResult(cases[0].expression, []))).toEqual([ [html.Text, 'You have ', 0], [html.Element, 'b', 0], [html.Text, 'no', 1], [html.Text, ' messages', 0], ]); expect(humanizeDom(new ParseTreeResult(cases[1].expression, []))).toEqual([ [html.Text, 'One {{message}}', 0] ]); }); it('should normalize line-endings in expansion forms in inline templates if `i18nNormalizeLineEndingsInICUs` is true', () => { const parsed = parser.parse( `
\r\n` + ` {\r\n` + ` messages.length,\r\n` + ` plural,\r\n` + ` =0 {You have \r\nno\r\n messages}\r\n` + ` =1 {One {{message}}}}\r\n` + `
`, 'TestComp', { tokenizeExpansionForms: true, escapedString: true, i18nNormalizeLineEndingsInICUs: true, }); expect(humanizeDom(parsed)).toEqual([ [html.Element, 'div', 0], [html.Text, '\n ', 1], [html.Expansion, '\n messages.length', 'plural', 1], [html.ExpansionCase, '=0', 2], [html.ExpansionCase, '=1', 2], [html.Text, '\n', 1], ]); const cases = (parsed.rootNodes[0]).children[1].cases; expect(humanizeDom(new ParseTreeResult(cases[0].expression, []))).toEqual([ [html.Text, 'You have \nno\n messages', 0], ]); expect(humanizeDom(new ParseTreeResult(cases[1].expression, []))).toEqual([ [html.Text, 'One {{message}}', 0] ]); expect(parsed.errors).toEqual([]); }); it('should not normalize line-endings in ICU expressions in external templates when `i18nNormalizeLineEndingsInICUs` is not set', () => { const parsed = parser.parse( `
\r\n` + ` {\r\n` + ` messages.length,\r\n` + ` plural,\r\n` + ` =0 {You have \r\nno\r\n messages}\r\n` + ` =1 {One {{message}}}}\r\n` + `
`, 'TestComp', {tokenizeExpansionForms: true, escapedString: true}); expect(humanizeDom(parsed)).toEqual([ [html.Element, 'div', 0], [html.Text, '\n ', 1], [html.Expansion, '\r\n messages.length', 'plural', 1], [html.ExpansionCase, '=0', 2], [html.ExpansionCase, '=1', 2], [html.Text, '\n', 1], ]); const cases = (parsed.rootNodes[0]).children[1].cases; expect(humanizeDom(new ParseTreeResult(cases[0].expression, []))).toEqual([ [html.Text, 'You have \nno\n messages', 0], ]); expect(humanizeDom(new ParseTreeResult(cases[1].expression, []))).toEqual([ [html.Text, 'One {{message}}', 0] ]); expect(parsed.errors).toEqual([]); }); it('should normalize line-endings in expansion forms in external templates if `i18nNormalizeLineEndingsInICUs` is true', () => { const parsed = parser.parse( `
\r\n` + ` {\r\n` + ` messages.length,\r\n` + ` plural,\r\n` + ` =0 {You have \r\nno\r\n messages}\r\n` + ` =1 {One {{message}}}}\r\n` + `
`, 'TestComp', { tokenizeExpansionForms: true, escapedString: false, i18nNormalizeLineEndingsInICUs: true }); expect(humanizeDom(parsed)).toEqual([ [html.Element, 'div', 0], [html.Text, '\n ', 1], [html.Expansion, '\n messages.length', 'plural', 1], [html.ExpansionCase, '=0', 2], [html.ExpansionCase, '=1', 2], [html.Text, '\n', 1], ]); const cases = (parsed.rootNodes[0]).children[1].cases; expect(humanizeDom(new ParseTreeResult(cases[0].expression, []))).toEqual([ [html.Text, 'You have \nno\n messages', 0], ]); expect(humanizeDom(new ParseTreeResult(cases[1].expression, []))).toEqual([ [html.Text, 'One {{message}}', 0] ]); expect(parsed.errors).toEqual([]); }); it('should not normalize line-endings in ICU expressions in external templates when `i18nNormalizeLineEndingsInICUs` is not set', () => { const parsed = parser.parse( `
\r\n` + ` {\r\n` + ` messages.length,\r\n` + ` plural,\r\n` + ` =0 {You have \r\nno\r\n messages}\r\n` + ` =1 {One {{message}}}}\r\n` + `
`, 'TestComp', {tokenizeExpansionForms: true, escapedString: false}); expect(humanizeDom(parsed)).toEqual([ [html.Element, 'div', 0], [html.Text, '\n ', 1], [html.Expansion, '\r\n messages.length', 'plural', 1], [html.ExpansionCase, '=0', 2], [html.ExpansionCase, '=1', 2], [html.Text, '\n', 1], ]); const cases = (parsed.rootNodes[0]).children[1].cases; expect(humanizeDom(new ParseTreeResult(cases[0].expression, []))).toEqual([ [html.Text, 'You have \nno\n messages', 0], ]); expect(humanizeDom(new ParseTreeResult(cases[1].expression, []))).toEqual([ [html.Text, 'One {{message}}', 0] ]); expect(parsed.errors).toEqual([]); }); it('should parse out expansion forms', () => { const parsed = parser.parse( `
{a, plural, =0 {b}}
`, 'TestComp', {tokenizeExpansionForms: true}); expect(humanizeDom(parsed)).toEqual([ [html.Element, 'div', 0], [html.Element, 'span', 1], [html.Expansion, 'a', 'plural', 2], [html.ExpansionCase, '=0', 3], ]); }); it('should parse out nested expansion forms', () => { const parsed = parser.parse( `{messages.length, plural, =0 { {p.gender, select, male {m}} }}`, 'TestComp', {tokenizeExpansionForms: true}); expect(humanizeDom(parsed)).toEqual([ [html.Expansion, 'messages.length', 'plural', 0], [html.ExpansionCase, '=0', 1], ]); const firstCase = (parsed.rootNodes[0]).cases[0]; expect(humanizeDom(new ParseTreeResult(firstCase.expression, []))).toEqual([ [html.Expansion, 'p.gender', 'select', 0], [html.ExpansionCase, 'male', 1], [html.Text, ' ', 0], ]); }); it('should normalize line endings in nested expansion forms for inline templates, when `i18nNormalizeLineEndingsInICUs` is true', () => { const parsed = parser.parse( `{\r\n` + ` messages.length, plural,\r\n` + ` =0 { zero \r\n` + ` {\r\n` + ` p.gender, select,\r\n` + ` male {m}\r\n` + ` }\r\n` + ` }\r\n` + `}`, 'TestComp', { tokenizeExpansionForms: true, escapedString: true, i18nNormalizeLineEndingsInICUs: true }); expect(humanizeDom(parsed)).toEqual([ [html.Expansion, '\n messages.length', 'plural', 0], [html.ExpansionCase, '=0', 1], ]); const expansion = parsed.rootNodes[0] as html.Expansion; expect(humanizeDom(new ParseTreeResult(expansion.cases[0].expression, []))).toEqual([ [html.Text, 'zero \n ', 0], [html.Expansion, '\n p.gender', 'select', 0], [html.ExpansionCase, 'male', 1], [html.Text, '\n ', 0], ]); expect(parsed.errors).toEqual([]); }); it('should not normalize line endings in nested expansion forms for inline templates, when `i18nNormalizeLineEndingsInICUs` is not defined', () => { const parsed = parser.parse( `{\r\n` + ` messages.length, plural,\r\n` + ` =0 { zero \r\n` + ` {\r\n` + ` p.gender, select,\r\n` + ` male {m}\r\n` + ` }\r\n` + ` }\r\n` + `}`, 'TestComp', {tokenizeExpansionForms: true, escapedString: true}); expect(humanizeDom(parsed)).toEqual([ [html.Expansion, '\r\n messages.length', 'plural', 0], [html.ExpansionCase, '=0', 1], ]); const expansion = parsed.rootNodes[0] as html.Expansion; expect(humanizeDom(new ParseTreeResult(expansion.cases[0].expression, []))).toEqual([ [html.Text, 'zero \n ', 0], [html.Expansion, '\r\n p.gender', 'select', 0], [html.ExpansionCase, 'male', 1], [html.Text, '\n ', 0], ]); expect(parsed.errors).toEqual([]); }); it('should not normalize line endings in nested expansion forms for external templates, when `i18nNormalizeLineEndingsInICUs` is not set', () => { const parsed = parser.parse( `{\r\n` + ` messages.length, plural,\r\n` + ` =0 { zero \r\n` + ` {\r\n` + ` p.gender, select,\r\n` + ` male {m}\r\n` + ` }\r\n` + ` }\r\n` + `}`, 'TestComp', {tokenizeExpansionForms: true}); expect(humanizeDom(parsed)).toEqual([ [html.Expansion, '\r\n messages.length', 'plural', 0], [html.ExpansionCase, '=0', 1], ]); const expansion = parsed.rootNodes[0] as html.Expansion; expect(humanizeDom(new ParseTreeResult(expansion.cases[0].expression, []))).toEqual([ [html.Text, 'zero \n ', 0], [html.Expansion, '\r\n p.gender', 'select', 0], [html.ExpansionCase, 'male', 1], [html.Text, '\n ', 0], ]); expect(parsed.errors).toEqual([]); }); it('should error when expansion form is not closed', () => { const p = parser.parse( `{messages.length, plural, =0 {one}`, 'TestComp', {tokenizeExpansionForms: true}); expect(humanizeErrors(p.errors)).toEqual([ [null, 'Invalid ICU message. Missing \'}\'.', '0:34'] ]); }); it('should support ICU expressions with cases that contain numbers', () => { const p = parser.parse( `{sex, select, male {m} female {f} 0 {other}}`, 'TestComp', {tokenizeExpansionForms: true}); expect(p.errors.length).toEqual(0); }); it(`should support ICU expressions with cases that contain any character except '}'`, () => { const p = parser.parse( `{a, select, b {foo} % bar {% bar}}`, 'TestComp', {tokenizeExpansionForms: true}); expect(p.errors.length).toEqual(0); }); it('should error when expansion case is not properly closed', () => { const p = parser.parse( `{a, select, b {foo} % { bar {% bar}}`, 'TestComp', {tokenizeExpansionForms: true}); expect(humanizeErrors(p.errors)).toEqual([ [ TokenType.RAW_TEXT, 'Unexpected character "EOF" (Do you have an unescaped "{" in your template? Use "{{ \'{\' }}") to escape it.)', '0:36' ], [null, 'Invalid ICU message. Missing \'}\'.', '0:22'] ]); }); it('should error when expansion case is not closed', () => { const p = parser.parse( `{messages.length, plural, =0 {one`, 'TestComp', {tokenizeExpansionForms: true}); expect(humanizeErrors(p.errors)).toEqual([ [null, 'Invalid ICU message. Missing \'}\'.', '0:29'] ]); }); it('should error when invalid html in the case', () => { const p = parser.parse( `{messages.length, plural, =0 {}`, 'TestComp', {tokenizeExpansionForms: true}); expect(humanizeErrors(p.errors)).toEqual([ ['b', 'Only void and foreign elements can be self closed "b"', '0:30'] ]); }); }); describe('source spans', () => { it('should store the location', () => { expect(humanizeDomSourceSpans(parser.parse( '
\na\n
', 'TestComp'))) .toEqual([ [ html.Element, 'div', 0, '
\na\n
', '
', '
' ], [html.Attribute, '[prop]', 'v1', '[prop]="v1"'], [html.Attribute, '(e)', 'do()', '(e)="do()"'], [html.Attribute, 'attr', 'v2', 'attr="v2"'], [html.Attribute, 'noValue', '', 'noValue'], [html.Text, '\na\n', 1, '\na\n'], ]); }); it('should set the start and end source spans', () => { const node = parser.parse('
a
', 'TestComp').rootNodes[0]; expect(node.startSourceSpan.start.offset).toEqual(0); expect(node.startSourceSpan.end.offset).toEqual(5); expect(node.endSourceSpan!.start.offset).toEqual(6); expect(node.endSourceSpan!.end.offset).toEqual(12); }); it('should not set the end source span for void elements', () => { expect(humanizeDomSourceSpans(parser.parse('

', 'TestComp'))).toEqual([ [html.Element, 'div', 0, '

', '
', '
'], [html.Element, 'br', 1, '
', '
', null], ]); }); it('should not set the end source span for multiple void elements', () => { expect(humanizeDomSourceSpans(parser.parse('


', 'TestComp'))).toEqual([ [html.Element, 'div', 0, '


', '
', '
'], [html.Element, 'br', 1, '
', '
', null], [html.Element, 'hr', 1, '
', '
', null], ]); }); it('should not set the end source span for standalone void elements', () => { expect(humanizeDomSourceSpans(parser.parse('
', 'TestComp'))).toEqual([ [html.Element, 'br', 0, '
', '
', null], ]); }); it('should set the end source span for standalone self-closing elements', () => { expect(humanizeDomSourceSpans(parser.parse('
', 'TestComp'))).toEqual([ [html.Element, 'br', 0, '
', '
', '
'], ]); }); it('should set the end source span for self-closing elements', () => { expect(humanizeDomSourceSpans(parser.parse('

', 'TestComp'))).toEqual([ [html.Element, 'div', 0, '

', '
', '
'], [html.Element, 'br', 1, '
', '
', '
'], ]); }); it('should not set the end source span for elements that are implicitly closed', () => { expect(humanizeDomSourceSpans(parser.parse('

', 'TestComp'))).toEqual([ [html.Element, 'div', 0, '

', '
', '
'], [html.Element, 'p', 1, '

', '

', null], ]); expect(humanizeDomSourceSpans(parser.parse('

  • A
  • B
  • ', 'TestComp'))) .toEqual([ [html.Element, 'div', 0, '
  • A
  • B
  • ', '
    ', '
    '], [html.Element, 'li', 1, '
  • ', '
  • ', null], [html.Text, 'A', 2, 'A'], [html.Element, 'li', 1, '
  • ', '
  • ', null], [html.Text, 'B', 2, 'B'], ]); }); it('should support expansion form', () => { expect(humanizeDomSourceSpans(parser.parse( '
    {count, plural, =0 {msg}}
    ', 'TestComp', {tokenizeExpansionForms: true}))) .toEqual([ [html.Element, 'div', 0, '
    {count, plural, =0 {msg}}
    ', '
    ', '
    '], [html.Expansion, 'count', 'plural', 1, '{count, plural, =0 {msg}}'], [html.ExpansionCase, '=0', 2, '=0 {msg}'], ]); }); it('should not report a value span for an attribute without a value', () => { const ast = parser.parse('
    ', 'TestComp'); expect((ast.rootNodes[0] as html.Element).attrs[0].valueSpan).toBeUndefined(); }); it('should report a value span for an attribute with a value', () => { const ast = parser.parse('
    ', 'TestComp'); const attr = (ast.rootNodes[0] as html.Element).attrs[0]; expect(attr.valueSpan!.start.offset).toEqual(10); expect(attr.valueSpan!.end.offset).toEqual(12); }); it('should report a value span for an unquoted attribute value', () => { const ast = parser.parse('
    ', 'TestComp'); const attr = (ast.rootNodes[0] as html.Element).attrs[0]; expect(attr.valueSpan!.start.offset).toEqual(9); expect(attr.valueSpan!.end.offset).toEqual(11); }); }); describe('visitor', () => { it('should visit text nodes', () => { const result = humanizeDom(parser.parse('text', 'TestComp')); expect(result).toEqual([[html.Text, 'text', 0]]); }); it('should visit element nodes', () => { const result = humanizeDom(parser.parse('
    ', 'TestComp')); expect(result).toEqual([[html.Element, 'div', 0]]); }); it('should visit attribute nodes', () => { const result = humanizeDom(parser.parse('
    ', 'TestComp')); expect(result).toContain([html.Attribute, 'id', 'foo']); }); it('should visit all nodes', () => { const result = parser.parse('
    ab
    ', 'TestComp'); const accumulator: html.Node[] = []; const visitor = new class { visit(node: html.Node, context: any) { accumulator.push(node); } visitElement(element: html.Element, context: any): any { html.visitAll(this, element.attrs); html.visitAll(this, element.children); } visitAttribute(attribute: html.Attribute, context: any): any {} visitText(text: html.Text, context: any): any {} visitComment(comment: html.Comment, context: any): any {} visitExpansion(expansion: html.Expansion, context: any): any { html.visitAll(this, expansion.cases); } visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any {} }; html.visitAll(visitor, result.rootNodes); expect(accumulator.map(n => n.constructor)).toEqual([ html.Element, html.Attribute, html.Element, html.Attribute, html.Text, html.Element, html.Text ]); }); it('should skip typed visit if visit() returns a truthy value', () => { const visitor = new class { visit(node: html.Node, context: any) { return true; } visitElement(element: html.Element, context: any): any { throw Error('Unexpected'); } visitAttribute(attribute: html.Attribute, context: any): any { throw Error('Unexpected'); } visitText(text: html.Text, context: any): any { throw Error('Unexpected'); } visitComment(comment: html.Comment, context: any): any { throw Error('Unexpected'); } visitExpansion(expansion: html.Expansion, context: any): any { throw Error('Unexpected'); } visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any { throw Error('Unexpected'); } }; const result = parser.parse('
    ', 'TestComp'); const traversal = html.visitAll(visitor, result.rootNodes); expect(traversal).toEqual([true, true]); }); }); describe('errors', () => { it('should report unexpected closing tags', () => { const errors = parser.parse('

    ', 'TestComp').errors; expect(errors.length).toEqual(1); expect(humanizeErrors(errors)).toEqual([[ 'p', 'Unexpected closing tag "p". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags', '0:5' ]]); }); describe('incomplete element tag', () => { it('should parse and report incomplete tags after the tag name', () => { const {errors, rootNodes} = parser.parse('
    ', 'TestComp'); expect(humanizeNodes(rootNodes, true)).toEqual([ [html.Element, 'div', 0, '
    ', '', ''], [html.Element, 'div', 1, '
    { const {errors, rootNodes} = parser.parse('
    ', 'TestComp'); expect(humanizeNodes(rootNodes, true)).toEqual([ [html.Element, 'div', 0, '
    ', '', ''], ]); expect(humanizeErrors(errors)).toEqual([ ['div', 'Opening tag "div" not terminated.', '0:0'], ]); }); it('should parse and report incomplete tags after quote', () => { const {errors, rootNodes} = parser.parse('
    ', 'TestComp'); expect(humanizeNodes(rootNodes, true)).toEqual([ [html.Element, 'div', 0, '
    ', '', ''], ]); expect(humanizeErrors(errors)).toEqual([ ['div', 'Opening tag "div" not terminated.', '0:0'], ]); }); it('should report subsequent open tags without proper close tag', () => { const errors = parser.parse('', 'TestComp').errors; expect(errors.length).toEqual(2); expect(humanizeErrors(errors)).toEqual([ ['div', 'Opening tag "div" not terminated.', '0:0'], // TODO(ayazhafiz): the following error is unnecessary and can be pruned if we keep // track of the incomplete tag names. [ 'div', 'Unexpected closing tag "div". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags', '0:4' ] ]); }); }); it('should report closing tag for void elements', () => { const errors = parser.parse('', 'TestComp').errors; expect(errors.length).toEqual(1); expect(humanizeErrors(errors)).toEqual([ ['input', 'Void elements do not have end tags "input"', '0:7'] ]); }); it('should report self closing html element', () => { const errors = parser.parse('

    ', 'TestComp').errors; expect(errors.length).toEqual(1); expect(humanizeErrors(errors)).toEqual([ ['p', 'Only void and foreign elements can be self closed "p"', '0:0'] ]); }); it('should report self closing custom element', () => { const errors = parser.parse('', 'TestComp').errors; expect(errors.length).toEqual(1); expect(humanizeErrors(errors)).toEqual([ ['my-cmp', 'Only void and foreign elements can be self closed "my-cmp"', '0:0'] ]); }); it('should also report lexer errors', () => { const errors = parser.parse('

    ', 'TestComp').errors; expect(errors.length).toEqual(2); expect(humanizeErrors(errors)).toEqual([ [TokenType.COMMENT_START, 'Unexpected character "e"', '0:3'], [ 'p', 'Unexpected closing tag "p". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags', '0:14' ] ]); }); }); }); }); } export function humanizeErrors(errors: ParseError[]): any[] { return errors.map(e => { if (e instanceof TreeError) { // Parser errors return [e.elementName, e.msg, humanizeLineColumn(e.span.start)]; } // Tokenizer errors return [(e).tokenType, e.msg, humanizeLineColumn(e.span.start)]; }); }