refactor(compiler): html_parser -> ml_parser
This commit is contained in:
@ -0,0 +1,96 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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 {beforeEach, ddescribe, describe, expect, it} from '../../../core/testing/testing_internal';
|
||||
import * as html from '../../src/ml_parser/ast';
|
||||
import {HtmlParser} from '../../src/ml_parser/html_parser';
|
||||
|
||||
export function main() {
|
||||
describe('Node serilaizer', () => {
|
||||
var parser: HtmlParser;
|
||||
|
||||
beforeEach(() => { parser = new HtmlParser(); });
|
||||
|
||||
it('should support element', () => {
|
||||
const html = '<p></p>';
|
||||
const ast = parser.parse(html, 'url');
|
||||
expect(serializeAst(ast.rootNodes)).toEqual([html]);
|
||||
});
|
||||
|
||||
it('should support attributes', () => {
|
||||
const html = '<p k="value"></p>';
|
||||
const ast = parser.parse(html, 'url');
|
||||
expect(serializeAst(ast.rootNodes)).toEqual([html]);
|
||||
});
|
||||
|
||||
it('should support text', () => {
|
||||
const html = 'some text';
|
||||
const ast = parser.parse(html, 'url');
|
||||
expect(serializeAst(ast.rootNodes)).toEqual([html]);
|
||||
});
|
||||
|
||||
it('should support expansion', () => {
|
||||
const html = '{number, plural, =0 {none} =1 {one} other {many}}';
|
||||
const ast = parser.parse(html, 'url', true);
|
||||
expect(serializeAst(ast.rootNodes)).toEqual([html]);
|
||||
});
|
||||
|
||||
it('should support comment', () => {
|
||||
const html = '<!--comment-->';
|
||||
const ast = parser.parse(html, 'url', true);
|
||||
expect(serializeAst(ast.rootNodes)).toEqual([html]);
|
||||
});
|
||||
|
||||
it('should support nesting', () => {
|
||||
const html = `<div i18n="meaning|desc">
|
||||
<span>{{ interpolation }}</span>
|
||||
<!--comment-->
|
||||
<p expansion="true">
|
||||
{number, plural, =0 {{sex, gender, other {<b>?</b>}}}}
|
||||
</p>
|
||||
</div>`;
|
||||
const ast = parser.parse(html, 'url', true);
|
||||
expect(serializeAst(ast.rootNodes)).toEqual([html]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _SerializerVisitor implements html.Visitor {
|
||||
visitElement(element: html.Element, context: any): any {
|
||||
return `<${element.name}${this._visitAll(element.attrs, ' ')}>${this._visitAll(element.children)}</${element.name}>`;
|
||||
}
|
||||
|
||||
visitAttribute(attribute: html.Attribute, context: any): any {
|
||||
return `${attribute.name}="${attribute.value}"`;
|
||||
}
|
||||
|
||||
visitText(text: html.Text, context: any): any { return text.value; }
|
||||
|
||||
visitComment(comment: html.Comment, context: any): any { return `<!--${comment.value}-->`; }
|
||||
|
||||
visitExpansion(expansion: html.Expansion, context: any): any {
|
||||
return `{${expansion.switchValue}, ${expansion.type},${this._visitAll(expansion.cases)}}`;
|
||||
}
|
||||
|
||||
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any {
|
||||
return ` ${expansionCase.value} {${this._visitAll(expansionCase.expression)}}`;
|
||||
}
|
||||
|
||||
private _visitAll(nodes: html.Node[], join: string = ''): string {
|
||||
if (nodes.length == 0) {
|
||||
return '';
|
||||
}
|
||||
return join + nodes.map(a => a.visit(this, null)).join(join);
|
||||
}
|
||||
}
|
||||
|
||||
const serializerVisitor = new _SerializerVisitor();
|
||||
|
||||
export function serializeAst(nodes: html.Node[]): string[] {
|
||||
return nodes.map(node => node.visit(serializerVisitor, null));
|
||||
}
|
85
modules/@angular/compiler/test/ml_parser/ast_spec_utils.ts
Normal file
85
modules/@angular/compiler/test/ml_parser/ast_spec_utils.ts
Normal file
@ -0,0 +1,85 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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 {BaseException} from '../../src/facade/exceptions';
|
||||
import * as html from '../../src/ml_parser/ast';
|
||||
import {ParseTreeResult} from '../../src/ml_parser/html_parser';
|
||||
import {ParseLocation} from '../../src/parse_util';
|
||||
|
||||
export function humanizeDom(parseResult: ParseTreeResult, addSourceSpan: boolean = false): any[] {
|
||||
if (parseResult.errors.length > 0) {
|
||||
var errorString = parseResult.errors.join('\n');
|
||||
throw new BaseException(`Unexpected parse errors:\n${errorString}`);
|
||||
}
|
||||
|
||||
return humanizeNodes(parseResult.rootNodes, addSourceSpan);
|
||||
}
|
||||
|
||||
export function humanizeDomSourceSpans(parseResult: ParseTreeResult): any[] {
|
||||
return humanizeDom(parseResult, true);
|
||||
}
|
||||
|
||||
export function humanizeNodes(nodes: html.Node[], addSourceSpan: boolean = false): any[] {
|
||||
var humanizer = new _Humanizer(addSourceSpan);
|
||||
html.visitAll(humanizer, nodes);
|
||||
return humanizer.result;
|
||||
}
|
||||
|
||||
export function humanizeLineColumn(location: ParseLocation): string {
|
||||
return `${location.line}:${location.col}`;
|
||||
}
|
||||
|
||||
class _Humanizer implements html.Visitor {
|
||||
result: any[] = [];
|
||||
elDepth: number = 0;
|
||||
|
||||
constructor(private includeSourceSpan: boolean){};
|
||||
|
||||
visitElement(element: html.Element, context: any): any {
|
||||
var res = this._appendContext(element, [html.Element, element.name, this.elDepth++]);
|
||||
this.result.push(res);
|
||||
html.visitAll(this, element.attrs);
|
||||
html.visitAll(this, element.children);
|
||||
this.elDepth--;
|
||||
}
|
||||
|
||||
visitAttribute(attribute: html.Attribute, context: any): any {
|
||||
var res = this._appendContext(attribute, [html.Attribute, attribute.name, attribute.value]);
|
||||
this.result.push(res);
|
||||
}
|
||||
|
||||
visitText(text: html.Text, context: any): any {
|
||||
var res = this._appendContext(text, [html.Text, text.value, this.elDepth]);
|
||||
this.result.push(res);
|
||||
}
|
||||
|
||||
visitComment(comment: html.Comment, context: any): any {
|
||||
var res = this._appendContext(comment, [html.Comment, comment.value, this.elDepth]);
|
||||
this.result.push(res);
|
||||
}
|
||||
|
||||
visitExpansion(expansion: html.Expansion, context: any): any {
|
||||
var res = this._appendContext(
|
||||
expansion, [html.Expansion, expansion.switchValue, expansion.type, this.elDepth++]);
|
||||
this.result.push(res);
|
||||
html.visitAll(this, expansion.cases);
|
||||
this.elDepth--;
|
||||
}
|
||||
|
||||
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any {
|
||||
var res =
|
||||
this._appendContext(expansionCase, [html.ExpansionCase, expansionCase.value, this.elDepth]);
|
||||
this.result.push(res);
|
||||
}
|
||||
|
||||
private _appendContext(ast: html.Node, input: any[]): any[] {
|
||||
if (!this.includeSourceSpan) return input;
|
||||
input.push(ast.sourceSpan.toString());
|
||||
return input;
|
||||
}
|
||||
}
|
440
modules/@angular/compiler/test/ml_parser/html_parser_spec.ts
Normal file
440
modules/@angular/compiler/test/ml_parser/html_parser_spec.ts
Normal file
@ -0,0 +1,440 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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 {afterEach, beforeEach, ddescribe, describe, expect, iit, it, xit} from '../../../core/testing/testing_internal';
|
||||
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} from './ast_spec_utils';
|
||||
|
||||
export function main() {
|
||||
describe('HtmlParser', () => {
|
||||
var 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('<div>a</div>', 'TestComp'))).toEqual([
|
||||
[html.Element, 'div', 0], [html.Text, 'a', 1]
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse text nodes inside template elements', () => {
|
||||
expect(humanizeDom(parser.parse('<template>a</template>', 'TestComp'))).toEqual([
|
||||
[html.Element, 'template', 0], [html.Text, 'a', 1]
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse CDATA', () => {
|
||||
expect(humanizeDom(parser.parse('<![CDATA[text]]>', 'TestComp'))).toEqual([
|
||||
[html.Text, 'text', 0]
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('elements', () => {
|
||||
it('should parse root level elements', () => {
|
||||
expect(humanizeDom(parser.parse('<div></div>', 'TestComp'))).toEqual([
|
||||
[html.Element, 'div', 0]
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse elements inside of regular elements', () => {
|
||||
expect(humanizeDom(parser.parse('<div><span></span></div>', 'TestComp'))).toEqual([
|
||||
[html.Element, 'div', 0], [html.Element, 'span', 1]
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse elements inside of template elements', () => {
|
||||
expect(humanizeDom(parser.parse('<template><span></span></template>', 'TestComp')))
|
||||
.toEqual([[html.Element, 'template', 0], [html.Element, 'span', 1]]);
|
||||
});
|
||||
|
||||
it('should support void elements', () => {
|
||||
expect(humanizeDom(parser.parse('<link rel="author license" href="/about">', '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',
|
||||
() => { // http://www.w3.org/TR/html-markup/syntax.html#syntax-elements without:
|
||||
// <base> - it can be present in head only
|
||||
// <meta> - it can be present in head only
|
||||
// <command> - obsolete
|
||||
// <keygen> - obsolete
|
||||
['<map><area></map>', '<div><br></div>', '<colgroup><col></colgroup>',
|
||||
'<div><embed></div>', '<div><hr></div>', '<div><img></div>', '<div><input></div>',
|
||||
'<object><param>/<object>', '<audio><source></audio>', '<audio><track></audio>',
|
||||
'<p><wbr></p>',
|
||||
].forEach((html) => { expect(parser.parse(html, 'TestComp').errors).toEqual([]); });
|
||||
});
|
||||
|
||||
it('should close void elements on text nodes', () => {
|
||||
expect(humanizeDom(parser.parse('<p>before<br>after</p>', '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('<div><p>1<p>2</div>', '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('<ul><li><ul><li></li></ul></li></ul>', 'TestComp')))
|
||||
.toEqual([
|
||||
[html.Element, 'ul', 0],
|
||||
[html.Element, 'li', 1],
|
||||
[html.Element, 'ul', 2],
|
||||
[html.Element, 'li', 3],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should add the requiredParent', () => {
|
||||
expect(
|
||||
humanizeDom(parser.parse(
|
||||
'<table><thead><tr head></tr></thead><tr noparent></tr><tbody><tr body></tr></tbody><tfoot><tr foot></tr></tfoot></table>',
|
||||
'TestComp')))
|
||||
.toEqual([
|
||||
[html.Element, 'table', 0],
|
||||
[html.Element, 'thead', 1],
|
||||
[html.Element, 'tr', 2],
|
||||
[html.Attribute, 'head', ''],
|
||||
[html.Element, 'tbody', 1],
|
||||
[html.Element, 'tr', 2],
|
||||
[html.Attribute, 'noparent', ''],
|
||||
[html.Element, 'tbody', 1],
|
||||
[html.Element, 'tr', 2],
|
||||
[html.Attribute, 'body', ''],
|
||||
[html.Element, 'tfoot', 1],
|
||||
[html.Element, 'tr', 2],
|
||||
[html.Attribute, 'foot', ''],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should append the required parent considering ng-container', () => {
|
||||
expect(humanizeDom(parser.parse(
|
||||
'<table><ng-container><tr></tr></ng-container></table>', 'TestComp')))
|
||||
.toEqual([
|
||||
[html.Element, 'table', 0],
|
||||
[html.Element, 'tbody', 1],
|
||||
[html.Element, 'ng-container', 2],
|
||||
[html.Element, 'tr', 3],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should special case ng-container when adding a required parent', () => {
|
||||
expect(humanizeDom(parser.parse(
|
||||
'<table><thead><ng-container><tr></tr></ng-container></thead></table>',
|
||||
'TestComp')))
|
||||
.toEqual([
|
||||
[html.Element, 'table', 0],
|
||||
[html.Element, 'thead', 1],
|
||||
[html.Element, 'ng-container', 2],
|
||||
[html.Element, 'tr', 3],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not add the requiredParent when the parent is a template', () => {
|
||||
expect(humanizeDom(parser.parse('<template><tr></tr></template>', 'TestComp'))).toEqual([
|
||||
[html.Element, 'template', 0],
|
||||
[html.Element, 'tr', 1],
|
||||
]);
|
||||
});
|
||||
|
||||
// https://github.com/angular/angular/issues/5967
|
||||
it('should not add the requiredParent to a template root element', () => {
|
||||
expect(humanizeDom(parser.parse('<tr></tr>', 'TestComp'))).toEqual([
|
||||
[html.Element, 'tr', 0],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should support explicit mamespace', () => {
|
||||
expect(humanizeDom(parser.parse('<myns:div></myns:div>', 'TestComp'))).toEqual([
|
||||
[html.Element, ':myns:div', 0]
|
||||
]);
|
||||
});
|
||||
|
||||
it('should support implicit mamespace', () => {
|
||||
expect(humanizeDom(parser.parse('<svg></svg>', 'TestComp'))).toEqual([
|
||||
[html.Element, ':svg:svg', 0]
|
||||
]);
|
||||
});
|
||||
|
||||
it('should propagate the namespace', () => {
|
||||
expect(humanizeDom(parser.parse('<myns:div><p></p></myns:div>', 'TestComp'))).toEqual([
|
||||
[html.Element, ':myns:div', 0],
|
||||
[html.Element, ':myns:p', 1],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should match closing tags case sensitive', () => {
|
||||
let errors = parser.parse('<DiV><P></p></dIv>', 'TestComp').errors;
|
||||
expect(errors.length).toEqual(2);
|
||||
expect(humanizeErrors(errors)).toEqual([
|
||||
['p', 'Unexpected closing tag "p"', '0:8'],
|
||||
['dIv', 'Unexpected closing tag "dIv"', '0:12'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should support self closing void elements', () => {
|
||||
expect(humanizeDom(parser.parse('<input />', 'TestComp'))).toEqual([
|
||||
[html.Element, 'input', 0]
|
||||
]);
|
||||
});
|
||||
|
||||
it('should support self closing foreign elements', () => {
|
||||
expect(humanizeDom(parser.parse('<math />', 'TestComp'))).toEqual([
|
||||
[html.Element, ':math:math', 0]
|
||||
]);
|
||||
});
|
||||
|
||||
it('should ignore LF immediately after textarea, pre and listing', () => {
|
||||
expect(humanizeDom(parser.parse(
|
||||
'<p>\n</p><textarea>\n</textarea><pre>\n\n</pre><listing>\n\n</listing>',
|
||||
'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],
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('attributes', () => {
|
||||
it('should parse attributes on regular elements case sensitive', () => {
|
||||
expect(humanizeDom(parser.parse('<div kEy="v" key2=v2></div>', 'TestComp'))).toEqual([
|
||||
[html.Element, 'div', 0],
|
||||
[html.Attribute, 'kEy', 'v'],
|
||||
[html.Attribute, 'key2', 'v2'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse attributes without values', () => {
|
||||
expect(humanizeDom(parser.parse('<div k></div>', 'TestComp'))).toEqual([
|
||||
[html.Element, 'div', 0],
|
||||
[html.Attribute, 'k', ''],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse attributes on svg elements case sensitive', () => {
|
||||
expect(humanizeDom(parser.parse('<svg viewBox="0"></svg>', 'TestComp'))).toEqual([
|
||||
[html.Element, ':svg:svg', 0],
|
||||
[html.Attribute, 'viewBox', '0'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse attributes on template elements', () => {
|
||||
expect(humanizeDom(parser.parse('<template k="v"></template>', 'TestComp'))).toEqual([
|
||||
[html.Element, 'template', 0],
|
||||
[html.Attribute, 'k', 'v'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should support namespace', () => {
|
||||
expect(humanizeDom(parser.parse('<svg:use xlink:href="Port" />', 'TestComp'))).toEqual([
|
||||
[html.Element, ':svg:use', 0],
|
||||
[html.Attribute, ':xlink:href', 'Port'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('comments', () => {
|
||||
it('should preserve comments', () => {
|
||||
expect(humanizeDom(parser.parse('<!-- comment --><div></div>', 'TestComp'))).toEqual([
|
||||
[html.Comment, 'comment', 0],
|
||||
[html.Element, 'div', 0],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('expansion forms', () => {
|
||||
it('should parse out expansion forms', () => {
|
||||
let parsed = parser.parse(
|
||||
`<div>before{messages.length, plural, =0 {You have <b>no</b> messages} =1 {One {{message}}}}after</div>`,
|
||||
'TestComp', 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],
|
||||
]);
|
||||
let cases = (<any>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 parse out nested expansion forms', () => {
|
||||
let parsed = parser.parse(
|
||||
`{messages.length, plural, =0 { {p.gender, gender, =m {m}} }}`, 'TestComp', true);
|
||||
expect(humanizeDom(parsed)).toEqual([
|
||||
[html.Expansion, 'messages.length', 'plural', 0],
|
||||
[html.ExpansionCase, '=0', 1],
|
||||
]);
|
||||
|
||||
let firstCase = (<any>parsed.rootNodes[0]).cases[0];
|
||||
|
||||
expect(humanizeDom(new ParseTreeResult(firstCase.expression, []))).toEqual([
|
||||
[html.Expansion, 'p.gender', 'gender', 0],
|
||||
[html.ExpansionCase, '=m', 1],
|
||||
[html.Text, ' ', 0],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should error when expansion form is not closed', () => {
|
||||
let p = parser.parse(`{messages.length, plural, =0 {one}`, 'TestComp', true);
|
||||
expect(humanizeErrors(p.errors)).toEqual([
|
||||
[null, 'Invalid ICU message. Missing \'}\'.', '0:34']
|
||||
]);
|
||||
});
|
||||
|
||||
it('should error when expansion case is not closed', () => {
|
||||
let p = parser.parse(`{messages.length, plural, =0 {one`, 'TestComp', true);
|
||||
expect(humanizeErrors(p.errors)).toEqual([
|
||||
[null, 'Invalid ICU message. Missing \'}\'.', '0:29']
|
||||
]);
|
||||
});
|
||||
|
||||
it('should error when invalid html in the case', () => {
|
||||
let p = parser.parse(`{messages.length, plural, =0 {<b/>}`, 'TestComp', 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(
|
||||
'<div [prop]="v1" (e)="do()" attr="v2" noValue>\na\n</div>', 'TestComp')))
|
||||
.toEqual([
|
||||
[html.Element, 'div', 0, '<div [prop]="v1" (e)="do()" attr="v2" noValue>'],
|
||||
[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', () => {
|
||||
let node = <html.Element>parser.parse('<div>a</div>', '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 support expansion form', () => {
|
||||
expect(humanizeDomSourceSpans(
|
||||
parser.parse('<div>{count, plural, =0 {msg}}</div>', 'TestComp', true)))
|
||||
.toEqual([
|
||||
[html.Element, 'div', 0, '<div>'],
|
||||
[html.Expansion, 'count', 'plural', 1, '{count, plural, =0 {msg}}'],
|
||||
[html.ExpansionCase, '=0', 2, '=0 {msg}'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('errors', () => {
|
||||
it('should report unexpected closing tags', () => {
|
||||
let errors = parser.parse('<div></p></div>', 'TestComp').errors;
|
||||
expect(errors.length).toEqual(1);
|
||||
expect(humanizeErrors(errors)).toEqual([['p', 'Unexpected closing tag "p"', '0:5']]);
|
||||
});
|
||||
|
||||
it('should report subsequent open tags without proper close tag', () => {
|
||||
let errors = parser.parse('<div</div>', 'TestComp').errors;
|
||||
expect(errors.length).toEqual(1);
|
||||
expect(humanizeErrors(errors)).toEqual([['div', 'Unexpected closing tag "div"', '0:4']]);
|
||||
});
|
||||
|
||||
it('should report closing tag for void elements', () => {
|
||||
let errors = parser.parse('<input></input>', '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', () => {
|
||||
let errors = parser.parse('<p />', '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', () => {
|
||||
let errors = parser.parse('<my-cmp />', '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', () => {
|
||||
let errors = parser.parse('<!-err--><div></p></div>', 'TestComp').errors;
|
||||
expect(errors.length).toEqual(2);
|
||||
expect(humanizeErrors(errors)).toEqual([
|
||||
[TokenType.COMMENT_START, 'Unexpected character "e"', '0:3'],
|
||||
['p', 'Unexpected closing tag "p"', '0:14']
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function humanizeErrors(errors: ParseError[]): any[] {
|
||||
return errors.map(e => {
|
||||
if (e instanceof TreeError) {
|
||||
// Parser errors
|
||||
return [<any>e.elementName, e.msg, humanizeLineColumn(e.span.start)];
|
||||
}
|
||||
// Tokenizer errors
|
||||
return [(<any>e).tokenType, e.msg, humanizeLineColumn(e.span.start)];
|
||||
});
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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 {ddescribe, describe, expect, iit, it} from '../../../core/testing/testing_internal';
|
||||
import * as html from '../../src/ml_parser/ast';
|
||||
import {HtmlParser} from '../../src/ml_parser/html_parser';
|
||||
import {ExpansionResult, expandNodes} from '../../src/ml_parser/icu_ast_expander';
|
||||
import {ParseError} from '../../src/parse_util';
|
||||
|
||||
import {humanizeNodes} from './ast_spec_utils';
|
||||
|
||||
export function main() {
|
||||
describe('Expander', () => {
|
||||
function expand(template: string): ExpansionResult {
|
||||
const htmlParser = new HtmlParser();
|
||||
const res = htmlParser.parse(template, 'url', true);
|
||||
return expandNodes(res.rootNodes);
|
||||
}
|
||||
|
||||
it('should handle the plural expansion form', () => {
|
||||
const res = expand(`{messages.length, plural,=0 {zero<b>bold</b>}}`);
|
||||
|
||||
expect(humanizeNodes(res.nodes)).toEqual([
|
||||
[html.Element, 'ng-container', 0],
|
||||
[html.Attribute, '[ngPlural]', 'messages.length'],
|
||||
[html.Element, 'template', 1],
|
||||
[html.Attribute, 'ngPluralCase', '=0'],
|
||||
[html.Text, 'zero', 2],
|
||||
[html.Element, 'b', 2],
|
||||
[html.Text, 'bold', 3],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle nested expansion forms', () => {
|
||||
const res = expand(`{messages.length, plural, =0 { {p.gender, gender, =m {m}} }}`);
|
||||
|
||||
expect(humanizeNodes(res.nodes)).toEqual([
|
||||
[html.Element, 'ng-container', 0],
|
||||
[html.Attribute, '[ngPlural]', 'messages.length'],
|
||||
[html.Element, 'template', 1],
|
||||
[html.Attribute, 'ngPluralCase', '=0'],
|
||||
[html.Element, 'ng-container', 2],
|
||||
[html.Attribute, '[ngSwitch]', 'p.gender'],
|
||||
[html.Element, 'template', 3],
|
||||
[html.Attribute, 'ngSwitchCase', '=m'],
|
||||
[html.Text, 'm', 4],
|
||||
[html.Text, ' ', 2],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should correctly set source code positions', () => {
|
||||
const nodes = expand(`{messages.length, plural,=0 {<b>bold</b>}}`).nodes;
|
||||
|
||||
const container: html.Element = <html.Element>nodes[0];
|
||||
expect(container.sourceSpan.start.col).toEqual(0);
|
||||
expect(container.sourceSpan.end.col).toEqual(42);
|
||||
expect(container.startSourceSpan.start.col).toEqual(0);
|
||||
expect(container.startSourceSpan.end.col).toEqual(42);
|
||||
expect(container.endSourceSpan.start.col).toEqual(0);
|
||||
expect(container.endSourceSpan.end.col).toEqual(42);
|
||||
|
||||
const switchExp = container.attrs[0];
|
||||
expect(switchExp.sourceSpan.start.col).toEqual(1);
|
||||
expect(switchExp.sourceSpan.end.col).toEqual(16);
|
||||
|
||||
const template: html.Element = <html.Element>container.children[0];
|
||||
expect(template.sourceSpan.start.col).toEqual(25);
|
||||
expect(template.sourceSpan.end.col).toEqual(41);
|
||||
|
||||
const switchCheck = template.attrs[0];
|
||||
expect(switchCheck.sourceSpan.start.col).toEqual(25);
|
||||
expect(switchCheck.sourceSpan.end.col).toEqual(28);
|
||||
|
||||
const b: html.Element = <html.Element>template.children[0];
|
||||
expect(b.sourceSpan.start.col).toEqual(29);
|
||||
expect(b.endSourceSpan.end.col).toEqual(40);
|
||||
});
|
||||
|
||||
it('should handle other special forms', () => {
|
||||
const res = expand(`{person.gender, gender,=male {m}}`);
|
||||
|
||||
expect(humanizeNodes(res.nodes)).toEqual([
|
||||
[html.Element, 'ng-container', 0],
|
||||
[html.Attribute, '[ngSwitch]', 'person.gender'],
|
||||
[html.Element, 'template', 1],
|
||||
[html.Attribute, 'ngSwitchCase', '=male'],
|
||||
[html.Text, 'm', 2],
|
||||
]);
|
||||
});
|
||||
|
||||
describe('errors', () => {
|
||||
it('should error on unknown plural cases', () => {
|
||||
expect(humanizeErrors(expand('{n, plural, unknown {-}}').errors)).toEqual([
|
||||
`Plural cases should be "=<number>" or one of zero, one, two, few, many, other`,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function humanizeErrors(errors: ParseError[]): string[] {
|
||||
return errors.map(error => error.msg);
|
||||
}
|
803
modules/@angular/compiler/test/ml_parser/lexer_spec.ts
Normal file
803
modules/@angular/compiler/test/ml_parser/lexer_spec.ts
Normal file
@ -0,0 +1,803 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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 {afterEach, beforeEach, ddescribe, describe, expect, iit, it, xit} from '../../../core/testing/testing_internal';
|
||||
import {getHtmlTagDefinition} from '../../src/ml_parser/html_tags';
|
||||
import {InterpolationConfig} from '../../src/ml_parser/interpolation_config';
|
||||
import * as lex from '../../src/ml_parser/lexer';
|
||||
import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_util';
|
||||
|
||||
export function main() {
|
||||
describe('HtmlLexer', () => {
|
||||
describe('line/column numbers', () => {
|
||||
it('should work without newlines', () => {
|
||||
expect(tokenizeAndHumanizeLineColumn('<t>a</t>')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, '0:0'],
|
||||
[lex.TokenType.TAG_OPEN_END, '0:2'],
|
||||
[lex.TokenType.TEXT, '0:3'],
|
||||
[lex.TokenType.TAG_CLOSE, '0:4'],
|
||||
[lex.TokenType.EOF, '0:8'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should work with one newline', () => {
|
||||
expect(tokenizeAndHumanizeLineColumn('<t>\na</t>')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, '0:0'],
|
||||
[lex.TokenType.TAG_OPEN_END, '0:2'],
|
||||
[lex.TokenType.TEXT, '0:3'],
|
||||
[lex.TokenType.TAG_CLOSE, '1:1'],
|
||||
[lex.TokenType.EOF, '1:5'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should work with multiple newlines', () => {
|
||||
expect(tokenizeAndHumanizeLineColumn('<t\n>\na</t>')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, '0:0'],
|
||||
[lex.TokenType.TAG_OPEN_END, '1:0'],
|
||||
[lex.TokenType.TEXT, '1:1'],
|
||||
[lex.TokenType.TAG_CLOSE, '2:1'],
|
||||
[lex.TokenType.EOF, '2:5'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should work with CR and LF', () => {
|
||||
expect(tokenizeAndHumanizeLineColumn('<t\n>\r\na\r</t>')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, '0:0'],
|
||||
[lex.TokenType.TAG_OPEN_END, '1:0'],
|
||||
[lex.TokenType.TEXT, '1:1'],
|
||||
[lex.TokenType.TAG_CLOSE, '2:1'],
|
||||
[lex.TokenType.EOF, '2:5'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('comments', () => {
|
||||
it('should parse comments', () => {
|
||||
expect(tokenizeAndHumanizeParts('<!--t\ne\rs\r\nt-->')).toEqual([
|
||||
[lex.TokenType.COMMENT_START],
|
||||
[lex.TokenType.RAW_TEXT, 't\ne\ns\nt'],
|
||||
[lex.TokenType.COMMENT_END],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should store the locations', () => {
|
||||
expect(tokenizeAndHumanizeSourceSpans('<!--t\ne\rs\r\nt-->')).toEqual([
|
||||
[lex.TokenType.COMMENT_START, '<!--'],
|
||||
[lex.TokenType.RAW_TEXT, 't\ne\rs\r\nt'],
|
||||
[lex.TokenType.COMMENT_END, '-->'],
|
||||
[lex.TokenType.EOF, ''],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should report <!- without -', () => {
|
||||
expect(tokenizeAndHumanizeErrors('<!-a')).toEqual([
|
||||
[lex.TokenType.COMMENT_START, 'Unexpected character "a"', '0:3']
|
||||
]);
|
||||
});
|
||||
|
||||
it('should report missing end comment', () => {
|
||||
expect(tokenizeAndHumanizeErrors('<!--')).toEqual([
|
||||
[lex.TokenType.RAW_TEXT, 'Unexpected character "EOF"', '0:4']
|
||||
]);
|
||||
});
|
||||
|
||||
it('should accept comments finishing by too many dashes (even number)', () => {
|
||||
expect(tokenizeAndHumanizeSourceSpans('<!-- test ---->')).toEqual([
|
||||
[lex.TokenType.COMMENT_START, '<!--'],
|
||||
[lex.TokenType.RAW_TEXT, ' test --'],
|
||||
[lex.TokenType.COMMENT_END, '-->'],
|
||||
[lex.TokenType.EOF, ''],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should accept comments finishing by too many dashes (odd number)', () => {
|
||||
expect(tokenizeAndHumanizeSourceSpans('<!-- test --->')).toEqual([
|
||||
[lex.TokenType.COMMENT_START, '<!--'],
|
||||
[lex.TokenType.RAW_TEXT, ' test -'],
|
||||
[lex.TokenType.COMMENT_END, '-->'],
|
||||
[lex.TokenType.EOF, ''],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('doctype', () => {
|
||||
it('should parse doctypes', () => {
|
||||
expect(tokenizeAndHumanizeParts('<!doctype html>')).toEqual([
|
||||
[lex.TokenType.DOC_TYPE, 'doctype html'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should store the locations', () => {
|
||||
expect(tokenizeAndHumanizeSourceSpans('<!doctype html>')).toEqual([
|
||||
[lex.TokenType.DOC_TYPE, '<!doctype html>'],
|
||||
[lex.TokenType.EOF, ''],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should report missing end doctype', () => {
|
||||
expect(tokenizeAndHumanizeErrors('<!')).toEqual([
|
||||
[lex.TokenType.DOC_TYPE, 'Unexpected character "EOF"', '0:2']
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CDATA', () => {
|
||||
it('should parse CDATA', () => {
|
||||
expect(tokenizeAndHumanizeParts('<![CDATA[t\ne\rs\r\nt]]>')).toEqual([
|
||||
[lex.TokenType.CDATA_START],
|
||||
[lex.TokenType.RAW_TEXT, 't\ne\ns\nt'],
|
||||
[lex.TokenType.CDATA_END],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should store the locations', () => {
|
||||
expect(tokenizeAndHumanizeSourceSpans('<![CDATA[t\ne\rs\r\nt]]>')).toEqual([
|
||||
[lex.TokenType.CDATA_START, '<![CDATA['],
|
||||
[lex.TokenType.RAW_TEXT, 't\ne\rs\r\nt'],
|
||||
[lex.TokenType.CDATA_END, ']]>'],
|
||||
[lex.TokenType.EOF, ''],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should report <![ without CDATA[', () => {
|
||||
expect(tokenizeAndHumanizeErrors('<![a')).toEqual([
|
||||
[lex.TokenType.CDATA_START, 'Unexpected character "a"', '0:3']
|
||||
]);
|
||||
});
|
||||
|
||||
it('should report missing end cdata', () => {
|
||||
expect(tokenizeAndHumanizeErrors('<![CDATA[')).toEqual([
|
||||
[lex.TokenType.RAW_TEXT, 'Unexpected character "EOF"', '0:9']
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('open tags', () => {
|
||||
it('should parse open tags without prefix', () => {
|
||||
expect(tokenizeAndHumanizeParts('<test>')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 'test'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse namespace prefix', () => {
|
||||
expect(tokenizeAndHumanizeParts('<ns1:test>')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, 'ns1', 'test'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse void tags', () => {
|
||||
expect(tokenizeAndHumanizeParts('<test/>')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 'test'],
|
||||
[lex.TokenType.TAG_OPEN_END_VOID],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should allow whitespace after the tag name', () => {
|
||||
expect(tokenizeAndHumanizeParts('<test >')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 'test'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should store the locations', () => {
|
||||
expect(tokenizeAndHumanizeSourceSpans('<test>')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, '<test'],
|
||||
[lex.TokenType.TAG_OPEN_END, '>'],
|
||||
[lex.TokenType.EOF, ''],
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('attributes', () => {
|
||||
it('should parse attributes without prefix', () => {
|
||||
expect(tokenizeAndHumanizeParts('<t a>')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 't'],
|
||||
[lex.TokenType.ATTR_NAME, null, 'a'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse attributes with interpolation', () => {
|
||||
expect(tokenizeAndHumanizeParts('<t a="{{v}}" b="s{{m}}e" c="s{{m//c}}e">')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 't'],
|
||||
[lex.TokenType.ATTR_NAME, null, 'a'],
|
||||
[lex.TokenType.ATTR_VALUE, '{{v}}'],
|
||||
[lex.TokenType.ATTR_NAME, null, 'b'],
|
||||
[lex.TokenType.ATTR_VALUE, 's{{m}}e'],
|
||||
[lex.TokenType.ATTR_NAME, null, 'c'],
|
||||
[lex.TokenType.ATTR_VALUE, 's{{m//c}}e'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse attributes with prefix', () => {
|
||||
expect(tokenizeAndHumanizeParts('<t ns1:a>')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 't'],
|
||||
[lex.TokenType.ATTR_NAME, 'ns1', 'a'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse attributes whose prefix is not valid', () => {
|
||||
expect(tokenizeAndHumanizeParts('<t (ns1:a)>')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 't'],
|
||||
[lex.TokenType.ATTR_NAME, null, '(ns1:a)'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse attributes with single quote value', () => {
|
||||
expect(tokenizeAndHumanizeParts('<t a=\'b\'>')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 't'],
|
||||
[lex.TokenType.ATTR_NAME, null, 'a'],
|
||||
[lex.TokenType.ATTR_VALUE, 'b'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse attributes with double quote value', () => {
|
||||
expect(tokenizeAndHumanizeParts('<t a="b">')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 't'],
|
||||
[lex.TokenType.ATTR_NAME, null, 'a'],
|
||||
[lex.TokenType.ATTR_VALUE, 'b'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse attributes with unquoted value', () => {
|
||||
expect(tokenizeAndHumanizeParts('<t a=b>')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 't'],
|
||||
[lex.TokenType.ATTR_NAME, null, 'a'],
|
||||
[lex.TokenType.ATTR_VALUE, 'b'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should allow whitespace', () => {
|
||||
expect(tokenizeAndHumanizeParts('<t a = b >')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 't'],
|
||||
[lex.TokenType.ATTR_NAME, null, 'a'],
|
||||
[lex.TokenType.ATTR_VALUE, 'b'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse attributes with entities in values', () => {
|
||||
expect(tokenizeAndHumanizeParts('<t a="AA">')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 't'],
|
||||
[lex.TokenType.ATTR_NAME, null, 'a'],
|
||||
[lex.TokenType.ATTR_VALUE, 'AA'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not decode entities without trailing ";"', () => {
|
||||
expect(tokenizeAndHumanizeParts('<t a="&" b="c&&d">')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 't'],
|
||||
[lex.TokenType.ATTR_NAME, null, 'a'],
|
||||
[lex.TokenType.ATTR_VALUE, '&'],
|
||||
[lex.TokenType.ATTR_NAME, null, 'b'],
|
||||
[lex.TokenType.ATTR_VALUE, 'c&&d'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse attributes with "&" in values', () => {
|
||||
expect(tokenizeAndHumanizeParts('<t a="b && c &">')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 't'],
|
||||
[lex.TokenType.ATTR_NAME, null, 'a'],
|
||||
[lex.TokenType.ATTR_VALUE, 'b && c &'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse values with CR and LF', () => {
|
||||
expect(tokenizeAndHumanizeParts('<t a=\'t\ne\rs\r\nt\'>')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 't'],
|
||||
[lex.TokenType.ATTR_NAME, null, 'a'],
|
||||
[lex.TokenType.ATTR_VALUE, 't\ne\ns\nt'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should store the locations', () => {
|
||||
expect(tokenizeAndHumanizeSourceSpans('<t a=b>')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, '<t'],
|
||||
[lex.TokenType.ATTR_NAME, 'a'],
|
||||
[lex.TokenType.ATTR_VALUE, 'b'],
|
||||
[lex.TokenType.TAG_OPEN_END, '>'],
|
||||
[lex.TokenType.EOF, ''],
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('closing tags', () => {
|
||||
it('should parse closing tags without prefix', () => {
|
||||
expect(tokenizeAndHumanizeParts('</test>')).toEqual([
|
||||
[lex.TokenType.TAG_CLOSE, null, 'test'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse closing tags with prefix', () => {
|
||||
expect(tokenizeAndHumanizeParts('</ns1:test>')).toEqual([
|
||||
[lex.TokenType.TAG_CLOSE, 'ns1', 'test'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should allow whitespace', () => {
|
||||
expect(tokenizeAndHumanizeParts('</ test >')).toEqual([
|
||||
[lex.TokenType.TAG_CLOSE, null, 'test'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should store the locations', () => {
|
||||
expect(tokenizeAndHumanizeSourceSpans('</test>')).toEqual([
|
||||
[lex.TokenType.TAG_CLOSE, '</test>'],
|
||||
[lex.TokenType.EOF, ''],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should report missing name after </', () => {
|
||||
expect(tokenizeAndHumanizeErrors('</')).toEqual([
|
||||
[lex.TokenType.TAG_CLOSE, 'Unexpected character "EOF"', '0:2']
|
||||
]);
|
||||
});
|
||||
|
||||
it('should report missing >', () => {
|
||||
expect(tokenizeAndHumanizeErrors('</test')).toEqual([
|
||||
[lex.TokenType.TAG_CLOSE, 'Unexpected character "EOF"', '0:6']
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('entities', () => {
|
||||
it('should parse named entities', () => {
|
||||
expect(tokenizeAndHumanizeParts('a&b')).toEqual([
|
||||
[lex.TokenType.TEXT, 'a&b'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse hexadecimal entities', () => {
|
||||
expect(tokenizeAndHumanizeParts('AA')).toEqual([
|
||||
[lex.TokenType.TEXT, 'AA'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse decimal entities', () => {
|
||||
expect(tokenizeAndHumanizeParts('A')).toEqual([
|
||||
[lex.TokenType.TEXT, 'A'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should store the locations', () => {
|
||||
expect(tokenizeAndHumanizeSourceSpans('a&b')).toEqual([
|
||||
[lex.TokenType.TEXT, 'a&b'],
|
||||
[lex.TokenType.EOF, ''],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should report malformed/unknown entities', () => {
|
||||
expect(tokenizeAndHumanizeErrors('&tbo;')).toEqual([[
|
||||
lex.TokenType.TEXT,
|
||||
'Unknown entity "tbo" - use the "&#<decimal>;" or "&#x<hex>;" syntax', '0:0'
|
||||
]]);
|
||||
expect(tokenizeAndHumanizeErrors('&#asdf;')).toEqual([
|
||||
[lex.TokenType.TEXT, 'Unexpected character "s"', '0:3']
|
||||
]);
|
||||
expect(tokenizeAndHumanizeErrors('
sdf;')).toEqual([
|
||||
[lex.TokenType.TEXT, 'Unexpected character "s"', '0:4']
|
||||
]);
|
||||
|
||||
expect(tokenizeAndHumanizeErrors('઼')).toEqual([
|
||||
[lex.TokenType.TEXT, 'Unexpected character "EOF"', '0:6']
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('regular text', () => {
|
||||
it('should parse text', () => {
|
||||
expect(tokenizeAndHumanizeParts('a')).toEqual([
|
||||
[lex.TokenType.TEXT, 'a'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse interpolation', () => {
|
||||
expect(tokenizeAndHumanizeParts('{{ a }}b{{ c // comment }}')).toEqual([
|
||||
[lex.TokenType.TEXT, '{{ a }}b{{ c // comment }}'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse interpolation with custom markers', () => {
|
||||
expect(tokenizeAndHumanizeParts('{% a %}', null, {start: '{%', end: '%}'})).toEqual([
|
||||
[lex.TokenType.TEXT, '{% a %}'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle CR & LF', () => {
|
||||
expect(tokenizeAndHumanizeParts('t\ne\rs\r\nt')).toEqual([
|
||||
[lex.TokenType.TEXT, 't\ne\ns\nt'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse entities', () => {
|
||||
expect(tokenizeAndHumanizeParts('a&b')).toEqual([
|
||||
[lex.TokenType.TEXT, 'a&b'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse text starting with "&"', () => {
|
||||
expect(tokenizeAndHumanizeParts('a && b &')).toEqual([
|
||||
[lex.TokenType.TEXT, 'a && b &'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should store the locations', () => {
|
||||
expect(tokenizeAndHumanizeSourceSpans('a')).toEqual([
|
||||
[lex.TokenType.TEXT, 'a'],
|
||||
[lex.TokenType.EOF, ''],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should allow "<" in text nodes', () => {
|
||||
expect(tokenizeAndHumanizeParts('{{ a < b ? c : d }}')).toEqual([
|
||||
[lex.TokenType.TEXT, '{{ a < b ? c : d }}'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
|
||||
expect(tokenizeAndHumanizeSourceSpans('<p>a<b</p>')).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, '<p'],
|
||||
[lex.TokenType.TAG_OPEN_END, '>'],
|
||||
[lex.TokenType.TEXT, 'a<b'],
|
||||
[lex.TokenType.TAG_CLOSE, '</p>'],
|
||||
[lex.TokenType.EOF, ''],
|
||||
]);
|
||||
|
||||
expect(tokenizeAndHumanizeParts('< a>')).toEqual([
|
||||
[lex.TokenType.TEXT, '< a>'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse valid start tag in interpolation', () => {
|
||||
expect(tokenizeAndHumanizeParts('{{ a <b && c > d }}')).toEqual([
|
||||
[lex.TokenType.TEXT, '{{ a '],
|
||||
[lex.TokenType.TAG_OPEN_START, null, 'b'],
|
||||
[lex.TokenType.ATTR_NAME, null, '&&'],
|
||||
[lex.TokenType.ATTR_NAME, null, 'c'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.TEXT, ' d }}'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should be able to escape {', () => {
|
||||
expect(tokenizeAndHumanizeParts('{{ "{" }}')).toEqual([
|
||||
[lex.TokenType.TEXT, '{{ "{" }}'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should be able to escape {{', () => {
|
||||
expect(tokenizeAndHumanizeParts('{{ "{{" }}')).toEqual([
|
||||
[lex.TokenType.TEXT, '{{ "{{" }}'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
describe('raw text', () => {
|
||||
it('should parse text', () => {
|
||||
expect(tokenizeAndHumanizeParts(`<script>t\ne\rs\r\nt</script>`)).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 'script'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.RAW_TEXT, 't\ne\ns\nt'],
|
||||
[lex.TokenType.TAG_CLOSE, null, 'script'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not detect entities', () => {
|
||||
expect(tokenizeAndHumanizeParts(`<script>&</SCRIPT>`)).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 'script'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.RAW_TEXT, '&'],
|
||||
[lex.TokenType.TAG_CLOSE, null, 'script'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should ignore other opening tags', () => {
|
||||
expect(tokenizeAndHumanizeParts(`<script>a<div></script>`)).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 'script'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.RAW_TEXT, 'a<div>'],
|
||||
[lex.TokenType.TAG_CLOSE, null, 'script'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should ignore other closing tags', () => {
|
||||
expect(tokenizeAndHumanizeParts(`<script>a</test></script>`)).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 'script'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.RAW_TEXT, 'a</test>'],
|
||||
[lex.TokenType.TAG_CLOSE, null, 'script'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should store the locations', () => {
|
||||
expect(tokenizeAndHumanizeSourceSpans(`<script>a</script>`)).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, '<script'],
|
||||
[lex.TokenType.TAG_OPEN_END, '>'],
|
||||
[lex.TokenType.RAW_TEXT, 'a'],
|
||||
[lex.TokenType.TAG_CLOSE, '</script>'],
|
||||
[lex.TokenType.EOF, ''],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapable raw text', () => {
|
||||
it('should parse text', () => {
|
||||
expect(tokenizeAndHumanizeParts(`<title>t\ne\rs\r\nt</title>`)).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 'title'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.ESCAPABLE_RAW_TEXT, 't\ne\ns\nt'],
|
||||
[lex.TokenType.TAG_CLOSE, null, 'title'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should detect entities', () => {
|
||||
expect(tokenizeAndHumanizeParts(`<title>&</title>`)).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 'title'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.ESCAPABLE_RAW_TEXT, '&'],
|
||||
[lex.TokenType.TAG_CLOSE, null, 'title'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should ignore other opening tags', () => {
|
||||
expect(tokenizeAndHumanizeParts(`<title>a<div></title>`)).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 'title'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.ESCAPABLE_RAW_TEXT, 'a<div>'],
|
||||
[lex.TokenType.TAG_CLOSE, null, 'title'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should ignore other closing tags', () => {
|
||||
expect(tokenizeAndHumanizeParts(`<title>a</test></title>`)).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, null, 'title'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.ESCAPABLE_RAW_TEXT, 'a</test>'],
|
||||
[lex.TokenType.TAG_CLOSE, null, 'title'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should store the locations', () => {
|
||||
expect(tokenizeAndHumanizeSourceSpans(`<title>a</title>`)).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, '<title'],
|
||||
[lex.TokenType.TAG_OPEN_END, '>'],
|
||||
[lex.TokenType.ESCAPABLE_RAW_TEXT, 'a'],
|
||||
[lex.TokenType.TAG_CLOSE, '</title>'],
|
||||
[lex.TokenType.EOF, ''],
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('expansion forms', () => {
|
||||
it('should parse an expansion form', () => {
|
||||
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four} =5 {five} foo {bar} }', true))
|
||||
.toEqual([
|
||||
[lex.TokenType.EXPANSION_FORM_START],
|
||||
[lex.TokenType.RAW_TEXT, 'one.two'],
|
||||
[lex.TokenType.RAW_TEXT, 'three'],
|
||||
[lex.TokenType.EXPANSION_CASE_VALUE, '=4'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_START],
|
||||
[lex.TokenType.TEXT, 'four'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_END],
|
||||
[lex.TokenType.EXPANSION_CASE_VALUE, '=5'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_START],
|
||||
[lex.TokenType.TEXT, 'five'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_END],
|
||||
[lex.TokenType.EXPANSION_CASE_VALUE, 'foo'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_START],
|
||||
[lex.TokenType.TEXT, 'bar'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_END],
|
||||
[lex.TokenType.EXPANSION_FORM_END],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse an expansion form with text elements surrounding it', () => {
|
||||
expect(tokenizeAndHumanizeParts('before{one.two, three, =4 {four}}after', true)).toEqual([
|
||||
[lex.TokenType.TEXT, 'before'],
|
||||
[lex.TokenType.EXPANSION_FORM_START],
|
||||
[lex.TokenType.RAW_TEXT, 'one.two'],
|
||||
[lex.TokenType.RAW_TEXT, 'three'],
|
||||
[lex.TokenType.EXPANSION_CASE_VALUE, '=4'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_START],
|
||||
[lex.TokenType.TEXT, 'four'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_END],
|
||||
[lex.TokenType.EXPANSION_FORM_END],
|
||||
[lex.TokenType.TEXT, 'after'],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse an expansion forms with elements in it', () => {
|
||||
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four <b>a</b>}}', true)).toEqual([
|
||||
[lex.TokenType.EXPANSION_FORM_START],
|
||||
[lex.TokenType.RAW_TEXT, 'one.two'],
|
||||
[lex.TokenType.RAW_TEXT, 'three'],
|
||||
[lex.TokenType.EXPANSION_CASE_VALUE, '=4'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_START],
|
||||
[lex.TokenType.TEXT, 'four '],
|
||||
[lex.TokenType.TAG_OPEN_START, null, 'b'],
|
||||
[lex.TokenType.TAG_OPEN_END],
|
||||
[lex.TokenType.TEXT, 'a'],
|
||||
[lex.TokenType.TAG_CLOSE, null, 'b'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_END],
|
||||
[lex.TokenType.EXPANSION_FORM_END],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse an expansion forms containing an interpolation', () => {
|
||||
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four {{a}}}}', true)).toEqual([
|
||||
[lex.TokenType.EXPANSION_FORM_START],
|
||||
[lex.TokenType.RAW_TEXT, 'one.two'],
|
||||
[lex.TokenType.RAW_TEXT, 'three'],
|
||||
[lex.TokenType.EXPANSION_CASE_VALUE, '=4'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_START],
|
||||
[lex.TokenType.TEXT, 'four {{a}}'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_END],
|
||||
[lex.TokenType.EXPANSION_FORM_END],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse nested expansion forms', () => {
|
||||
expect(tokenizeAndHumanizeParts(`{one.two, three, =4 { {xx, yy, =x {one}} }}`, true))
|
||||
.toEqual([
|
||||
[lex.TokenType.EXPANSION_FORM_START],
|
||||
[lex.TokenType.RAW_TEXT, 'one.two'],
|
||||
[lex.TokenType.RAW_TEXT, 'three'],
|
||||
[lex.TokenType.EXPANSION_CASE_VALUE, '=4'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_START],
|
||||
[lex.TokenType.EXPANSION_FORM_START],
|
||||
[lex.TokenType.RAW_TEXT, 'xx'],
|
||||
[lex.TokenType.RAW_TEXT, 'yy'],
|
||||
[lex.TokenType.EXPANSION_CASE_VALUE, '=x'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_START],
|
||||
[lex.TokenType.TEXT, 'one'],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_END],
|
||||
[lex.TokenType.EXPANSION_FORM_END],
|
||||
[lex.TokenType.TEXT, ' '],
|
||||
[lex.TokenType.EXPANSION_CASE_EXP_END],
|
||||
[lex.TokenType.EXPANSION_FORM_END],
|
||||
[lex.TokenType.EOF],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('errors', () => {
|
||||
it('should report unescaped "{" on error', () => {
|
||||
expect(tokenizeAndHumanizeErrors(`<p>before { after</p>`, true)).toEqual([[
|
||||
lex.TokenType.RAW_TEXT,
|
||||
`Unexpected character "EOF" (Do you have an unescaped "{" in your template? Use "{{ '{' }}") to escape it.)`,
|
||||
'0:21',
|
||||
]]);
|
||||
});
|
||||
|
||||
it('should include 2 lines of context in message', () => {
|
||||
let src = '111\n222\n333\nE\n444\n555\n666\n';
|
||||
let file = new ParseSourceFile(src, 'file://');
|
||||
let location = new ParseLocation(file, 12, 123, 456);
|
||||
let span = new ParseSourceSpan(location, location);
|
||||
let error = new lex.TokenError('**ERROR**', null, span);
|
||||
expect(error.toString())
|
||||
.toEqual(`**ERROR** ("\n222\n333\n[ERROR ->]E\n444\n555\n"): file://@123:456`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unicode characters', () => {
|
||||
it('should support unicode characters', () => {
|
||||
expect(tokenizeAndHumanizeSourceSpans(`<p>İ</p>`)).toEqual([
|
||||
[lex.TokenType.TAG_OPEN_START, '<p'],
|
||||
[lex.TokenType.TAG_OPEN_END, '>'],
|
||||
[lex.TokenType.TEXT, 'İ'],
|
||||
[lex.TokenType.TAG_CLOSE, '</p>'],
|
||||
[lex.TokenType.EOF, ''],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function tokenizeWithoutErrors(
|
||||
input: string, tokenizeExpansionForms: boolean = false,
|
||||
interpolationConfig?: InterpolationConfig): lex.Token[] {
|
||||
var tokenizeResult = lex.tokenize(
|
||||
input, 'someUrl', getHtmlTagDefinition, tokenizeExpansionForms, interpolationConfig);
|
||||
|
||||
if (tokenizeResult.errors.length > 0) {
|
||||
const errorString = tokenizeResult.errors.join('\n');
|
||||
throw new Error(`Unexpected parse errors:\n${errorString}`);
|
||||
}
|
||||
|
||||
return tokenizeResult.tokens;
|
||||
}
|
||||
|
||||
function tokenizeAndHumanizeParts(
|
||||
input: string, tokenizeExpansionForms: boolean = false,
|
||||
interpolationConfig?: InterpolationConfig): any[] {
|
||||
return tokenizeWithoutErrors(input, tokenizeExpansionForms, interpolationConfig)
|
||||
.map(token => [<any>token.type].concat(token.parts));
|
||||
}
|
||||
|
||||
function tokenizeAndHumanizeSourceSpans(input: string): any[] {
|
||||
return tokenizeWithoutErrors(input).map(token => [<any>token.type, token.sourceSpan.toString()]);
|
||||
}
|
||||
|
||||
function humanizeLineColumn(location: ParseLocation): string {
|
||||
return `${location.line}:${location.col}`;
|
||||
}
|
||||
|
||||
function tokenizeAndHumanizeLineColumn(input: string): any[] {
|
||||
return tokenizeWithoutErrors(input).map(
|
||||
token => [<any>token.type, humanizeLineColumn(token.sourceSpan.start)]);
|
||||
}
|
||||
|
||||
function tokenizeAndHumanizeErrors(input: string, tokenizeExpansionForms: boolean = false): any[] {
|
||||
return lex.tokenize(input, 'someUrl', getHtmlTagDefinition, tokenizeExpansionForms)
|
||||
.errors.map(e => [<any>e.tokenType, e.msg, humanizeLineColumn(e.span.start)]);
|
||||
}
|
Reference in New Issue
Block a user