2015-10-13 03:29:13 -04:00
|
|
|
import {
|
|
|
|
ddescribe,
|
|
|
|
describe,
|
|
|
|
it,
|
|
|
|
iit,
|
|
|
|
xit,
|
|
|
|
expect,
|
|
|
|
beforeEach,
|
|
|
|
afterEach
|
|
|
|
} from 'angular2/testing_internal';
|
2015-08-25 18:36:02 -04:00
|
|
|
|
2015-11-10 18:56:25 -05:00
|
|
|
import {HtmlTokenType} from 'angular2/src/compiler/html_lexer';
|
|
|
|
import {HtmlParser, HtmlParseTreeResult, HtmlTreeError} from 'angular2/src/compiler/html_parser';
|
2015-08-25 18:36:02 -04:00
|
|
|
import {
|
|
|
|
HtmlAst,
|
|
|
|
HtmlAstVisitor,
|
|
|
|
HtmlElementAst,
|
|
|
|
HtmlAttrAst,
|
|
|
|
HtmlTextAst,
|
|
|
|
htmlVisitAll
|
2015-11-05 17:07:57 -05:00
|
|
|
} from 'angular2/src/compiler/html_ast';
|
2015-11-10 18:56:25 -05:00
|
|
|
import {ParseError, ParseLocation, ParseSourceSpan} from 'angular2/src/compiler/parse_util';
|
|
|
|
|
|
|
|
import {BaseException} from 'angular2/src/facade/exceptions';
|
2015-08-25 18:36:02 -04:00
|
|
|
|
|
|
|
export function main() {
|
2015-10-07 12:34:21 -04:00
|
|
|
describe('HtmlParser', () => {
|
2015-08-25 18:36:02 -04:00
|
|
|
var parser: HtmlParser;
|
|
|
|
beforeEach(() => { parser = new HtmlParser(); });
|
|
|
|
|
2015-10-07 12:34:21 -04:00
|
|
|
describe('parse', () => {
|
2015-09-11 16:35:46 -04:00
|
|
|
describe('text nodes', () => {
|
|
|
|
it('should parse root level text nodes', () => {
|
2015-12-01 16:01:05 -05:00
|
|
|
expect(humanizeDom(parser.parse('a', 'TestComp'))).toEqual([[HtmlTextAst, 'a', 0]]);
|
2015-09-11 16:35:46 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should parse text nodes inside regular elements', () => {
|
|
|
|
expect(humanizeDom(parser.parse('<div>a</div>', 'TestComp')))
|
2015-12-01 16:01:05 -05:00
|
|
|
.toEqual([[HtmlElementAst, 'div', 0], [HtmlTextAst, 'a', 1]]);
|
2015-09-11 16:35:46 -04:00
|
|
|
});
|
2015-08-25 18:36:02 -04:00
|
|
|
|
2015-09-11 16:35:46 -04:00
|
|
|
it('should parse text nodes inside template elements', () => {
|
|
|
|
expect(humanizeDom(parser.parse('<template>a</template>', 'TestComp')))
|
2015-12-01 16:01:05 -05:00
|
|
|
.toEqual([[HtmlElementAst, 'template', 0], [HtmlTextAst, 'a', 1]]);
|
2015-11-10 18:56:25 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should parse CDATA', () => {
|
|
|
|
expect(humanizeDom(parser.parse('<![CDATA[text]]>', 'TestComp')))
|
2015-12-01 16:01:05 -05:00
|
|
|
.toEqual([[HtmlTextAst, 'text', 0]]);
|
2015-09-11 16:35:46 -04:00
|
|
|
});
|
2015-08-25 18:36:02 -04:00
|
|
|
});
|
|
|
|
|
2015-09-11 16:35:46 -04:00
|
|
|
describe('elements', () => {
|
|
|
|
it('should parse root level elements', () => {
|
|
|
|
expect(humanizeDom(parser.parse('<div></div>', 'TestComp')))
|
2015-11-10 18:56:25 -05:00
|
|
|
.toEqual([[HtmlElementAst, 'div', 0]]);
|
2015-09-11 16:35:46 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should parse elements inside of regular elements', () => {
|
|
|
|
expect(humanizeDom(parser.parse('<div><span></span></div>', 'TestComp')))
|
2015-11-10 18:56:25 -05:00
|
|
|
.toEqual([[HtmlElementAst, 'div', 0], [HtmlElementAst, 'span', 1]]);
|
2015-09-11 16:35:46 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should parse elements inside of template elements', () => {
|
|
|
|
expect(humanizeDom(parser.parse('<template><span></span></template>', 'TestComp')))
|
2015-11-10 18:56:25 -05:00
|
|
|
.toEqual([[HtmlElementAst, 'template', 0], [HtmlElementAst, 'span', 1]]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should support void elements', () => {
|
|
|
|
expect(humanizeDom(parser.parse('<link rel="author license" href="/about">', 'TestComp')))
|
|
|
|
.toEqual([
|
|
|
|
[HtmlElementAst, 'link', 0],
|
|
|
|
[HtmlAttrAst, 'rel', 'author license'],
|
|
|
|
[HtmlAttrAst, 'href', '/about'],
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
2015-12-07 12:06:03 -05:00
|
|
|
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([]); });
|
|
|
|
});
|
|
|
|
|
2015-12-01 16:01:05 -05:00
|
|
|
it('should close void elements on text nodes', () => {
|
|
|
|
expect(humanizeDom(parser.parse('<p>before<br>after</p>', 'TestComp')))
|
|
|
|
.toEqual([
|
|
|
|
[HtmlElementAst, 'p', 0],
|
|
|
|
[HtmlTextAst, 'before', 1],
|
|
|
|
[HtmlElementAst, 'br', 1],
|
|
|
|
[HtmlTextAst, 'after', 1],
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
2015-11-10 18:56:25 -05:00
|
|
|
it('should support optional end tags', () => {
|
|
|
|
expect(humanizeDom(parser.parse('<div><p>1<p>2</div>', 'TestComp')))
|
|
|
|
.toEqual([
|
|
|
|
[HtmlElementAst, 'div', 0],
|
|
|
|
[HtmlElementAst, 'p', 1],
|
2015-12-01 16:01:05 -05:00
|
|
|
[HtmlTextAst, '1', 2],
|
2015-11-10 18:56:25 -05:00
|
|
|
[HtmlElementAst, 'p', 1],
|
2015-12-01 16:01:05 -05:00
|
|
|
[HtmlTextAst, '2', 2],
|
2015-11-10 18:56:25 -05:00
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should support nested elements', () => {
|
|
|
|
expect(humanizeDom(parser.parse('<ul><li><ul><li></li></ul></li></ul>', 'TestComp')))
|
|
|
|
.toEqual([
|
|
|
|
[HtmlElementAst, 'ul', 0],
|
|
|
|
[HtmlElementAst, 'li', 1],
|
|
|
|
[HtmlElementAst, 'ul', 2],
|
|
|
|
[HtmlElementAst, 'li', 3],
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should add the requiredParent', () => {
|
2015-11-20 16:13:48 -05:00
|
|
|
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')))
|
2015-11-10 18:56:25 -05:00
|
|
|
.toEqual([
|
|
|
|
[HtmlElementAst, 'table', 0],
|
2015-11-20 16:13:48 -05:00
|
|
|
[HtmlElementAst, 'thead', 1],
|
|
|
|
[HtmlElementAst, 'tr', 2],
|
|
|
|
[HtmlAttrAst, 'head', ''],
|
|
|
|
[HtmlElementAst, 'tbody', 1],
|
|
|
|
[HtmlElementAst, 'tr', 2],
|
|
|
|
[HtmlAttrAst, 'noparent', ''],
|
2015-11-10 18:56:25 -05:00
|
|
|
[HtmlElementAst, 'tbody', 1],
|
|
|
|
[HtmlElementAst, 'tr', 2],
|
2015-11-20 16:13:48 -05:00
|
|
|
[HtmlAttrAst, 'body', ''],
|
|
|
|
[HtmlElementAst, 'tfoot', 1],
|
|
|
|
[HtmlElementAst, 'tr', 2],
|
|
|
|
[HtmlAttrAst, 'foot', '']
|
2015-11-10 18:56:25 -05:00
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
2015-12-07 12:41:01 -05:00
|
|
|
it('should not add the requiredParent when the parent is a template', () => {
|
|
|
|
expect(humanizeDom(parser.parse('<template><tr></tr></template>', 'TestComp')))
|
|
|
|
.toEqual([
|
|
|
|
[HtmlElementAst, 'template', 0],
|
|
|
|
[HtmlElementAst, 'tr', 1],
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
2015-11-10 18:56:25 -05:00
|
|
|
it('should support explicit mamespace', () => {
|
|
|
|
expect(humanizeDom(parser.parse('<myns:div></myns:div>', 'TestComp')))
|
|
|
|
.toEqual([[HtmlElementAst, '@myns:div', 0]]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should support implicit mamespace', () => {
|
|
|
|
expect(humanizeDom(parser.parse('<svg></svg>', 'TestComp')))
|
|
|
|
.toEqual([[HtmlElementAst, '@svg:svg', 0]]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should propagate the namespace', () => {
|
|
|
|
expect(humanizeDom(parser.parse('<myns:div><p></p></myns:div>', 'TestComp')))
|
|
|
|
.toEqual([[HtmlElementAst, '@myns:div', 0], [HtmlElementAst, '@myns:p', 1]]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should match closing tags case insensitive', () => {
|
|
|
|
expect(humanizeDom(parser.parse('<DiV><P></p></dIv>', 'TestComp')))
|
|
|
|
.toEqual([[HtmlElementAst, 'DiV', 0], [HtmlElementAst, 'P', 1]]);
|
2015-09-11 16:35:46 -04:00
|
|
|
});
|
2015-12-03 19:10:20 -05:00
|
|
|
|
|
|
|
it('should support self closing void elements', () => {
|
|
|
|
expect(humanizeDom(parser.parse('<input />', 'TestComp')))
|
|
|
|
.toEqual([[HtmlElementAst, 'input', 0]]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should support self closing foreign elements', () => {
|
|
|
|
expect(humanizeDom(parser.parse('<math />', 'TestComp')))
|
|
|
|
.toEqual([[HtmlElementAst, '@math:math', 0]]);
|
|
|
|
});
|
2015-08-25 18:36:02 -04:00
|
|
|
});
|
|
|
|
|
2015-09-11 16:35:46 -04:00
|
|
|
describe('attributes', () => {
|
2015-11-10 18:56:25 -05:00
|
|
|
it('should parse attributes on regular elements case sensitive', () => {
|
2015-10-07 12:34:21 -04:00
|
|
|
expect(humanizeDom(parser.parse('<div kEy="v" key2=v2></div>', 'TestComp')))
|
2015-09-11 16:35:46 -04:00
|
|
|
.toEqual([
|
2015-11-10 18:56:25 -05:00
|
|
|
[HtmlElementAst, 'div', 0],
|
2015-10-07 12:34:21 -04:00
|
|
|
[HtmlAttrAst, 'kEy', 'v'],
|
|
|
|
[HtmlAttrAst, 'key2', 'v2'],
|
2015-09-11 16:35:46 -04:00
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
2015-10-07 12:34:21 -04:00
|
|
|
it('should parse attributes without values', () => {
|
|
|
|
expect(humanizeDom(parser.parse('<div k></div>', 'TestComp')))
|
2015-11-10 18:56:25 -05:00
|
|
|
.toEqual([[HtmlElementAst, 'div', 0], [HtmlAttrAst, 'k', '']]);
|
2015-10-07 12:34:21 -04:00
|
|
|
});
|
|
|
|
|
2015-11-06 14:31:03 -05:00
|
|
|
it('should parse attributes on svg elements case sensitive', () => {
|
|
|
|
expect(humanizeDom(parser.parse('<svg viewBox="0"></svg>', 'TestComp')))
|
2015-11-10 18:56:25 -05:00
|
|
|
.toEqual([[HtmlElementAst, '@svg:svg', 0], [HtmlAttrAst, 'viewBox', '0']]);
|
2015-10-14 12:04:38 -04:00
|
|
|
});
|
|
|
|
|
2015-09-11 16:35:46 -04:00
|
|
|
it('should parse attributes on template elements', () => {
|
|
|
|
expect(humanizeDom(parser.parse('<template k="v"></template>', 'TestComp')))
|
2015-11-10 18:56:25 -05:00
|
|
|
.toEqual([[HtmlElementAst, 'template', 0], [HtmlAttrAst, 'k', 'v']]);
|
2015-09-11 16:35:46 -04:00
|
|
|
});
|
2015-08-25 18:36:02 -04:00
|
|
|
|
2015-11-10 18:56:25 -05:00
|
|
|
it('should support mamespace', () => {
|
2015-12-03 19:10:20 -05:00
|
|
|
expect(humanizeDom(parser.parse('<svg:use xlink:href="Port" />', 'TestComp')))
|
|
|
|
.toEqual([[HtmlElementAst, '@svg:use', 0], [HtmlAttrAst, '@xlink:href', 'Port']]);
|
2015-11-10 18:56:25 -05:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('comments', () => {
|
|
|
|
it('should ignore comments', () => {
|
|
|
|
expect(humanizeDom(parser.parse('<!-- comment --><div></div>', 'TestComp')))
|
|
|
|
.toEqual([[HtmlElementAst, 'div', 0]]);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
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([
|
|
|
|
[HtmlElementAst, 'div', 0, '<div [prop]="v1" (e)="do()" attr="v2" noValue>'],
|
|
|
|
[HtmlAttrAst, '[prop]', 'v1', '[prop]="v1"'],
|
|
|
|
[HtmlAttrAst, '(e)', 'do()', '(e)="do()"'],
|
|
|
|
[HtmlAttrAst, 'attr', 'v2', 'attr="v2"'],
|
|
|
|
[HtmlAttrAst, 'noValue', '', 'noValue'],
|
2015-12-01 16:01:05 -05:00
|
|
|
[HtmlTextAst, '\na\n', 1, '\na\n'],
|
2015-11-10 18:56:25 -05:00
|
|
|
]);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
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']]);
|
|
|
|
});
|
|
|
|
|
2015-12-03 18:53:44 -05:00
|
|
|
it('should report closing tag for void elements', () => {
|
|
|
|
let errors = parser.parse('<input></input>', 'TestComp').errors;
|
2015-12-02 13:11:01 -05:00
|
|
|
expect(errors.length).toEqual(1);
|
|
|
|
expect(humanizeErrors(errors))
|
2015-12-03 18:53:44 -05:00
|
|
|
.toEqual([['input', 'Void elements do not have end tags "input"', '0:7']]);
|
2015-12-02 13:11:01 -05:00
|
|
|
});
|
|
|
|
|
2015-12-03 19:10:20 -05:00
|
|
|
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']
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
2015-11-10 18:56:25 -05:00
|
|
|
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([
|
|
|
|
[HtmlTokenType.COMMENT_START, 'Unexpected character "e"', '0:3'],
|
|
|
|
['p', 'Unexpected closing tag "p"', '0:14']
|
|
|
|
]);
|
|
|
|
});
|
2015-08-25 18:36:02 -04:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2015-10-07 12:34:21 -04:00
|
|
|
function humanizeDom(parseResult: HtmlParseTreeResult): any[] {
|
|
|
|
if (parseResult.errors.length > 0) {
|
2015-11-10 18:56:25 -05:00
|
|
|
var errorString = parseResult.errors.join('\n');
|
|
|
|
throw new BaseException(`Unexpected parse errors:\n${errorString}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
var humanizer = new Humanizer(false);
|
|
|
|
htmlVisitAll(humanizer, parseResult.rootNodes);
|
|
|
|
return humanizer.result;
|
|
|
|
}
|
|
|
|
|
|
|
|
function humanizeDomSourceSpans(parseResult: HtmlParseTreeResult): any[] {
|
|
|
|
if (parseResult.errors.length > 0) {
|
|
|
|
var errorString = parseResult.errors.join('\n');
|
|
|
|
throw new BaseException(`Unexpected parse errors:\n${errorString}`);
|
2015-10-07 12:34:21 -04:00
|
|
|
}
|
2015-11-10 18:56:25 -05:00
|
|
|
|
|
|
|
var humanizer = new Humanizer(true);
|
2015-10-07 12:34:21 -04:00
|
|
|
htmlVisitAll(humanizer, parseResult.rootNodes);
|
2015-08-25 18:36:02 -04:00
|
|
|
return humanizer.result;
|
|
|
|
}
|
|
|
|
|
2015-11-10 18:56:25 -05:00
|
|
|
function humanizeLineColumn(location: ParseLocation): string {
|
|
|
|
return `${location.line}:${location.col}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
function humanizeErrors(errors: ParseError[]): any[] {
|
|
|
|
return errors.map(error => {
|
|
|
|
if (error instanceof HtmlTreeError) {
|
|
|
|
// Parser errors
|
|
|
|
return [<any>error.elementName, error.msg, humanizeLineColumn(error.location)];
|
|
|
|
}
|
|
|
|
// Tokenizer errors
|
|
|
|
return [(<any>error).tokenType, error.msg, humanizeLineColumn(error.location)];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2015-08-25 18:36:02 -04:00
|
|
|
class Humanizer implements HtmlAstVisitor {
|
|
|
|
result: any[] = [];
|
2015-11-10 18:56:25 -05:00
|
|
|
elDepth: number = 0;
|
|
|
|
|
|
|
|
constructor(private includeSourceSpan: boolean){};
|
2015-10-07 12:34:21 -04:00
|
|
|
|
2015-09-11 16:35:46 -04:00
|
|
|
visitElement(ast: HtmlElementAst, context: any): any {
|
2015-11-10 18:56:25 -05:00
|
|
|
var res = this._appendContext(ast, [HtmlElementAst, ast.name, this.elDepth++]);
|
|
|
|
this.result.push(res);
|
2015-08-25 18:36:02 -04:00
|
|
|
htmlVisitAll(this, ast.attrs);
|
|
|
|
htmlVisitAll(this, ast.children);
|
2015-11-10 18:56:25 -05:00
|
|
|
this.elDepth--;
|
2015-08-25 18:36:02 -04:00
|
|
|
return null;
|
|
|
|
}
|
2015-10-07 12:34:21 -04:00
|
|
|
|
2015-09-11 16:35:46 -04:00
|
|
|
visitAttr(ast: HtmlAttrAst, context: any): any {
|
2015-11-10 18:56:25 -05:00
|
|
|
var res = this._appendContext(ast, [HtmlAttrAst, ast.name, ast.value]);
|
|
|
|
this.result.push(res);
|
2015-08-25 18:36:02 -04:00
|
|
|
return null;
|
|
|
|
}
|
2015-10-07 12:34:21 -04:00
|
|
|
|
2015-09-11 16:35:46 -04:00
|
|
|
visitText(ast: HtmlTextAst, context: any): any {
|
2015-12-01 16:01:05 -05:00
|
|
|
var res = this._appendContext(ast, [HtmlTextAst, ast.value, this.elDepth]);
|
2015-11-10 18:56:25 -05:00
|
|
|
this.result.push(res);
|
2015-08-25 18:36:02 -04:00
|
|
|
return null;
|
|
|
|
}
|
2015-11-10 18:56:25 -05:00
|
|
|
|
|
|
|
private _appendContext(ast: HtmlAst, input: any[]): any[] {
|
|
|
|
if (!this.includeSourceSpan) return input;
|
|
|
|
input.push(ast.sourceSpan.toString());
|
|
|
|
return input;
|
|
|
|
}
|
2015-10-02 10:57:29 -04:00
|
|
|
}
|