', null],
[html.Attribute, 'p', '{{ abc', [''], ['{{', ' abc'], [''], 'p="{{ abc"'],
[html.Element, 'span', 1, '
', '
', ' '],
]);
expect(humanizeErrors(errors)).toEqual([]);
});
});
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, ['before']],
[html.Expansion, 'messages.length', 'plural', 1],
[html.ExpansionCase, '=0', 2],
[html.ExpansionCase, '=1', 2],
[html.Text, 'after', 1, ['after']],
]);
const cases = (
parsed.rootNodes[0]).children[1].cases;
expect(humanizeDom(new ParseTreeResult(cases[0].expression, []))).toEqual([
[html.Text, 'You have ', 0, ['You have ']],
[html.Element, 'b', 0],
[html.Text, 'no', 1, ['no']],
[html.Text, ' messages', 0, [' messages']],
]);
expect(humanizeDom(new ParseTreeResult(cases[1].expression, []))).toEqual([
[html.Text, 'One {{message}}', 0, ['One '], ['{{', 'message', '}}'], ['']]
]);
});
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, ['\n ']],
[html.Expansion, '\n messages.length', 'plural', 1],
[html.ExpansionCase, '=0', 2],
[html.ExpansionCase, '=1', 2],
[html.Text, '\n', 1, ['\n']],
]);
const cases = (parsed.rootNodes[0]).children[1].cases;
expect(humanizeDom(new ParseTreeResult(cases[0].expression, []))).toEqual([
[html.Text, 'You have \nno\n messages', 0, ['You have \nno\n messages']],
]);
expect(humanizeDom(new ParseTreeResult(cases[1].expression, []))).toEqual([
[html.Text, 'One {{message}}', 0, ['One '], ['{{', 'message', '}}'], ['']]
]);
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, ['\n ']],
[html.Expansion, '\r\n messages.length', 'plural', 1],
[html.ExpansionCase, '=0', 2],
[html.ExpansionCase, '=1', 2],
[html.Text, '\n', 1, ['\n']],
]);
const cases = (parsed.rootNodes[0]).children[1].cases;
expect(humanizeDom(new ParseTreeResult(cases[0].expression, []))).toEqual([
[html.Text, 'You have \nno\n messages', 0, ['You have \nno\n messages']],
]);
expect(humanizeDom(new ParseTreeResult(cases[1].expression, []))).toEqual([
[html.Text, 'One {{message}}', 0, ['One '], ['{{', 'message', '}}'], ['']]
]);
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, ['\n ']],
[html.Expansion, '\n messages.length', 'plural', 1],
[html.ExpansionCase, '=0', 2],
[html.ExpansionCase, '=1', 2],
[html.Text, '\n', 1, ['\n']],
]);
const cases = (parsed.rootNodes[0]).children[1].cases;
expect(humanizeDom(new ParseTreeResult(cases[0].expression, []))).toEqual([
[html.Text, 'You have \nno\n messages', 0, ['You have \nno\n messages']],
]);
expect(humanizeDom(new ParseTreeResult(cases[1].expression, []))).toEqual([
[html.Text, 'One {{message}}', 0, ['One '], ['{{', 'message', '}}'], ['']]
]);
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, ['\n ']],
[html.Expansion, '\r\n messages.length', 'plural', 1],
[html.ExpansionCase, '=0', 2],
[html.ExpansionCase, '=1', 2],
[html.Text, '\n', 1, ['\n']],
]);
const cases = (parsed.rootNodes[0]).children[1].cases;
expect(humanizeDom(new ParseTreeResult(cases[0].expression, []))).toEqual([
[html.Text, 'You have \nno\n messages', 0, ['You have \nno\n messages']],
]);
expect(humanizeDom(new ParseTreeResult(cases[1].expression, []))).toEqual([
[html.Text, 'One {{message}}', 0, ['One '], ['{{', 'message', '}}'], ['']]
]);
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, ['zero \n ']],
[html.Expansion, '\n p.gender', 'select', 0],
[html.ExpansionCase, 'male', 1],
[html.Text, '\n ', 0, ['\n ']],
]);
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, ['zero \n ']],
[html.Expansion, '\r\n p.gender', 'select', 0],
[html.ExpansionCase, 'male', 1],
[html.Text, '\n ', 0, ['\n ']],
]);
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, ['zero \n ']],
[html.Expansion, '\r\n p.gender', 'select', 0],
[html.ExpansionCase, 'male', 1],
[html.Text, '\n ', 0, ['\n ']],
]);
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, custom and foreign elements can be self closed "b"', '0:30']
]);
});
});
describe('blocks', () => {
// TODO(crisbeto): temporary utility while blocks are disabled by default.
const options = {tokenizeBlocks: true};
function humanizeBlocks(input: string): any[] {
return humanizeDom(parser.parse(input, 'TestComp', options));
}
it('should parse a block group', () => {
expect(humanizeBlocks('{#foo a b; c d}hello{/foo}')).toEqual([
[html.BlockGroup, 0],
[html.Block, 'foo', 1],
[html.BlockParameter, 'a b'],
[html.BlockParameter, 'c d'],
[html.Text, 'hello', 2, ['hello']],
]);
});
it('should parse a block group with an HTML element in the primary block', () => {
expect(humanizeBlocks('{#defer} {/defer}')).toEqual([
[html.BlockGroup, 0],
[html.Block, 'defer', 1],
[html.Element, 'my-cmp', 2],
]);
});
it('should parse a block group with blocks', () => {
expect(humanizeBlocks(
'{#defer when isVisible()} ' +
'{:loading after 100ms}Loading...' +
'{:placeholder minimum 50ms}Placeholder' +
'{/defer}'))
.toEqual([
[html.BlockGroup, 0],
[html.Block, 'defer', 1],
[html.BlockParameter, 'when isVisible()'],
[html.Element, 'my-cmp', 2],
[html.Block, 'loading', 1],
[html.BlockParameter, 'after 100ms'],
[html.Text, 'Loading...', 2, ['Loading...']],
[html.Block, 'placeholder', 1],
[html.BlockParameter, 'minimum 50ms'],
[html.Text, 'Placeholder', 2, ['Placeholder']],
]);
});
it('should parse a block group with blocks containing mixed plain text and HTML', () => {
expect(humanizeBlocks(
'{#defer when isVisible()}hello there' +
'{:loading after 100ms}Loading...
' +
'{:placeholder minimum 50ms}Placehol de r' +
'{/defer}',
))
.toEqual([
[html.BlockGroup, 0],
[html.Block, 'defer', 1],
[html.BlockParameter, 'when isVisible()'],
[html.Text, 'hello', 2, ['hello']],
[html.Element, 'my-cmp', 2],
[html.Text, 'there', 2, ['there']],
[html.Block, 'loading', 1],
[html.BlockParameter, 'after 100ms'],
[html.Element, 'p', 2],
[html.Text, 'Loading...', 3, ['Loading...']],
[html.Block, 'placeholder', 1],
[html.BlockParameter, 'minimum 50ms'],
[html.Text, 'P', 2, ['P']],
[html.Element, 'strong', 2],
[html.Text, 'laceh', 3, ['laceh']],
[html.Element, 'i', 3],
[html.Text, 'ol', 4, ['ol']],
[html.Text, 'de', 3, ['de']],
[html.Text, 'r', 2, ['r']],
]);
});
it('should parse nested block groups', () => {
// clang-format off
const markup = ` ` +
`{#root}` +
` ` +
`` +
`{#child}` +
`{:innerChild}` +
` ` +
`{#grandchild}` +
`{:innerGrandChild}` +
` ` +
`{:innerGrandChild}` +
` ` +
`{/grandchild}` +
`{:innerChild}` +
` ` +
`{/child}` +
` ` +
`{:outerChild}` +
` ` +
`{/root} `;
// clang-format on
expect(humanizeBlocks(markup)).toEqual([
[html.Element, 'root-sibling-one', 0],
[html.BlockGroup, 0],
[html.Block, 'root', 1],
[html.Element, 'outer-child-one', 2],
[html.Element, 'outer-child-two', 2],
[html.BlockGroup, 3],
[html.Block, 'child', 4],
[html.Block, 'innerChild', 4],
[html.Element, 'inner-child-one', 5],
[html.BlockGroup, 5],
[html.Block, 'grandchild', 6],
[html.Block, 'innerGrandChild', 6],
[html.Element, 'inner-grand-child-one', 7],
[html.Block, 'innerGrandChild', 6],
[html.Element, 'inner-grand-child-two', 7],
[html.Block, 'innerChild', 4],
[html.Element, 'inner-child-two', 5],
[html.Block, 'outerChild', 1],
[html.Element, 'outer-child-three', 2],
[html.Text, ' ', 0, [' ']],
[html.Element, 'root-sibling-two', 0],
]);
});
it('should infer namespace through block group boundary', () => {
expect(humanizeBlocks('{#if cond} {/if} ')).toEqual([
[html.Element, ':svg:svg', 0],
[html.BlockGroup, 1],
[html.Block, 'if', 2],
[html.BlockParameter, 'cond'],
[html.Element, ':svg:circle', 3],
]);
});
it('should create an implicit empty block if there is a block at the root', () => {
expect(humanizeBlocks(
'{#switch cond}' +
'{:case a}' +
'a case' +
'{:case b}' +
'b case' +
'{:default}' +
'no case' +
'{/switch}'))
.toEqual([
[html.BlockGroup, 0],
[html.Block, 'switch', 1],
[html.BlockParameter, 'cond'],
[html.Block, 'case', 1],
[html.BlockParameter, 'a'],
[html.Text, 'a case', 2, ['a case']],
[html.Block, 'case', 1],
[html.BlockParameter, 'b'],
[html.Text, 'b case', 2, ['b case']],
[html.Block, 'default', 1],
[html.Text, 'no case', 2, ['no case']],
]);
});
it('should parse a block group with empty blocks', () => {
expect(humanizeBlocks('{#foo}{:a}{:b}{:c}{/foo}')).toEqual([
[html.BlockGroup, 0],
[html.Block, 'foo', 1],
[html.Block, 'a', 1],
[html.Block, 'b', 1],
[html.Block, 'c', 1],
]);
});
it('should parse a block group with void elements', () => {
expect(humanizeBlocks('{#foo} {:bar} {/foo}')).toEqual([
[html.BlockGroup, 0],
[html.Block, 'foo', 1],
[html.Element, 'br', 2],
[html.Block, 'bar', 1],
[html.Element, 'img', 2],
]);
});
it('should close void elements used right before a block group', () => {
expect(humanizeBlocks(' {#foo}hello{/foo}')).toEqual([
[html.Element, 'img', 0],
[html.BlockGroup, 0],
[html.Block, 'foo', 1],
[html.Text, 'hello', 2, ['hello']],
]);
});
it('should report an unexpected block close', () => {
const errors = parser.parse('hello{/foo}', 'TestComp', options).errors;
expect(errors.length).toEqual(1);
expect(humanizeErrors(errors)).toEqual([[
'foo', 'Unexpected closing block "foo". The block may have been closed earlier.', '0:5'
]]);
});
it('should report unclosed tags inside of a block group', () => {
const errors = parser.parse('{#foo}hello{/foo}', 'TestComp', options).errors;
expect(errors.length).toEqual(1);
expect(humanizeErrors(errors)).toEqual([[
'foo',
'Unexpected closing block "foo". There is an unclosed "strong" HTML tag named that may have to be closed first.',
'0:19'
]]);
});
it('should report block used at the root', () => {
const errors = parser.parse('{:foo}hello', 'TestComp', options).errors;
expect(errors.length).toEqual(1);
expect(humanizeErrors(errors)).toEqual([
['foo', 'Blocks can only be placed inside of block groups.', '0:0']
]);
});
it('should report block used inside of an HTML element', () => {
const errors =
parser.parse('{#group}{:foo}hello
{/group}', 'TestComp', options).errors;
expect(errors.length).toEqual(1);
expect(humanizeErrors(errors)).toEqual([
['foo', 'Blocks can only be placed inside of block groups.', '0:13']
]);
});
it('should report an unexpected closing tag inside a block', () => {
const errors =
parser.parse('{#if cond}hello
{/if}', 'TestComp', options).errors;
expect(errors.length).toEqual(2);
expect(humanizeErrors(errors)).toEqual([
[
'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:20'
],
[
'if', 'Unexpected closing block "if". The block may have been closed earlier.', '0:26'
],
]);
});
it('should store the source locations of block groups and blocks', () => {
const markup = '{#defer when isVisible()}hello
world' +
'{:loading after 100ms}Loading...' +
'{:placeholder minimum 50ms}Placeholder ' +
'{/defer}';
expect(humanizeDomSourceSpans(parser.parse(markup, 'TestComp', options))).toEqual([
[html.BlockGroup, 0, markup, '{#defer when isVisible()}', '{/defer}'],
[
html.Block, 'defer', 1, '{#defer when isVisible()}hello
world',
'{#defer when isVisible()}', ''
],
[html.BlockParameter, 'when isVisible()', 'when isVisible()'],
[html.Element, 'div', 2, 'hello
', '', '
'],
[html.Text, 'hello', 3, ['hello'], 'hello'],
[html.Text, 'world', 2, ['world'], 'world'],
[
html.Block, 'loading', 1, '{:loading after 100ms}Loading...',
'{:loading after 100ms}', ''
],
[html.BlockParameter, 'after 100ms', 'after 100ms'],
[html.Text, 'Loading...', 2, ['Loading...'], 'Loading...'],
[
html.Block, 'placeholder', 1,
'{:placeholder minimum 50ms}Placeholder ',
'{:placeholder minimum 50ms}', ''
],
[html.BlockParameter, 'minimum 50ms', 'minimum 50ms'],
[html.Text, 'Placeholde', 2, ['Placeholde'], 'Placeholde'],
[html.Element, 'strong', 2, 'r ', '', ' '],
[html.Text, 'r', 3, ['r'], 'r'],
]);
});
});
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', ['v1'], '[prop]="v1"'],
[html.Attribute, '(e)', 'do()', ['do()'], '(e)="do()"'],
[html.Attribute, 'attr', 'v2', ['v2'], 'attr="v2"'],
[html.Attribute, 'noValue', '', 'noValue'],
[html.Text, '\na\n', 1, ['\na\n'], '\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);
});
// This checks backward compatibility with a previous version of the lexer, which would
// treat interpolation expressions as regular HTML escapable text.
it('should decode HTML entities in interpolations', () => {
expect(humanizeDomSourceSpans(parser.parse(
'{{&}}' +
'{{▾}}' +
'{{▾}}' +
'{{&unknown;}}' +
'{{& (no semi-colon)}}' +
'{{yz; (invalid hex)}}' +
'{{BE; (invalid decimal)}}',
'TestComp')))
.toEqual([[
html.Text,
'{{&}}' +
'{{\u25BE}}' +
'{{\u25BE}}' +
'{{&unknown;}}' +
'{{& (no semi-colon)}}' +
'{{yz; (invalid hex)}}' +
'{{BE; (invalid decimal)}}',
0,
[''],
['{{', '&', '}}'],
[''],
['{{', '▾', '}}'],
[''],
['{{', '▾', '}}'],
[''],
['{{', '&unknown;', '}}'],
[''],
['{{', '& (no semi-colon)', '}}'],
[''],
['{{', 'yz; (invalid hex)', '}}'],
[''],
['{{', 'BE; (invalid decimal)', '}}'],
[''],
'{{&}}' +
'{{▾}}' +
'{{▾}}' +
'{{&unknown;}}' +
'{{& (no semi-colon)}}' +
'{{yz; (invalid hex)}}' +
'{{BE; (invalid decimal)}}',
]]);
});
it('should support interpolations in text', () => {
expect(
humanizeDomSourceSpans(parser.parse(' pre {{ value }} post
', 'TestComp')))
.toEqual([
[html.Element, 'div', 0, ' pre {{ value }} post
', '', '
'],
[
html.Text, ' pre {{ value }} post ', 1, [' pre '], ['{{', ' value ', '}}'],
[' post '], ' pre {{ value }} post '
],
]);
});
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 include leading trivia from the following node of an element in the end source',
() => {
expect(humanizeDomSourceSpans(
parser.parse(' \n\n\n \n ', 'TestComp', {
leadingTriviaChars: [' ', '\n', '\r', '\t'],
})))
.toEqual([
[
html.Element, 'input', 0, ' ', ' ',
' '
],
[html.Attribute, 'type', 'text', ['text'], 'type="text"'],
[html.Text, '\n\n\n ', 0, ['\n\n\n '], '', '\n\n\n '],
[html.Element, 'span', 0, '\n ', '', ' '],
[html.Text, '\n', 1, ['\n'], '', '\n'],
]);
});
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'], 'A'],
[html.Element, 'li', 1, ' ', ' ', null],
[html.Text, 'B', 2, ['B'], '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, ['text']]]);
});
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', ['foo']]);
});
it('should visit all nodes', () => {
const result =
parser.parse('a b
', 'TestComp');
const accumulator: html.Node[] = [];
const visitor = new class implements html.Visitor {
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 {}
visitBlockGroup(group: html.BlockGroup, context: any) {
html.visitAll(this, group.blocks);
}
visitBlock(block: html.Block, context: any) {
html.visitAll(this, block.parameters);
html.visitAll(this, block.children);
}
visitBlockParameter(parameter: html.BlockParameter, context: 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 implements html.Visitor {
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');
}
visitBlockGroup(group: html.BlockGroup, context: any) {
throw Error('Unexpected');
}
visitBlock(block: html.Block, context: any) {
throw Error('Unexpected');
}
visitBlockParameter(parameter: html.BlockParameter, context: 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'
]]);
});
it('gets correct close tag for parent when a child is not closed', () => {
const {errors, rootNodes} = parser.parse('
', 'TestComp');
expect(errors.length).toEqual(1);
expect(humanizeErrors(errors)).toEqual([[
'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:11'
]]);
expect(humanizeNodes(rootNodes, true)).toEqual([
[html.Element, 'div', 0, '
', '', '
'],
[html.Element, 'span', 1, '', '', null],
]);
});
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, custom and foreign elements can be self closed "p"', '0:0']
]);
});
it('should not report self closing custom element', () => {
expect(parser.parse('
', 'TestComp').errors).toEqual([]);
});
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)];
});
}