From b2a23a9294567559058fcac3540a91641e7e2793 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Tue, 1 Dec 2015 13:01:05 -0800 Subject: [PATCH] fix(HtmlParser): close void elements on all node types fixes #5528 --- modules/angular2/src/compiler/html_parser.ts | 13 ++++++++++ modules/angular2/src/compiler/html_tags.ts | 26 ++++++++++--------- .../test/compiler/html_parser_spec.ts | 26 +++++++++++++------ 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/modules/angular2/src/compiler/html_parser.ts b/modules/angular2/src/compiler/html_parser.ts index 05d9fefe53bf2e..74c7f33485375d 100644 --- a/modules/angular2/src/compiler/html_parser.ts +++ b/modules/angular2/src/compiler/html_parser.ts @@ -61,12 +61,15 @@ class TreeBuilder { } else if (this.peek.type === HtmlTokenType.TAG_CLOSE) { this._consumeEndTag(this._advance()); } else if (this.peek.type === HtmlTokenType.CDATA_START) { + this._closeVoidElement(); this._consumeCdata(this._advance()); } else if (this.peek.type === HtmlTokenType.COMMENT_START) { + this._closeVoidElement(); this._consumeComment(this._advance()); } else if (this.peek.type === HtmlTokenType.TEXT || this.peek.type === HtmlTokenType.RAW_TEXT || this.peek.type === HtmlTokenType.ESCAPABLE_RAW_TEXT) { + this._closeVoidElement(); this._consumeText(this._advance()); } else { // Skip all other tokens... @@ -107,6 +110,16 @@ class TreeBuilder { this._addToParent(new HtmlTextAst(token.parts[0], token.sourceSpan)); } + private _closeVoidElement(): void { + if (this.elementStack.length > 0) { + let el = ListWrapper.last(this.elementStack); + + if (getHtmlTagDefinition(el.name).isVoid) { + this.elementStack.pop(); + } + } + } + private _consumeStartTag(startTagToken: HtmlToken) { var prefix = startTagToken.parts[0]; var name = startTagToken.parts[1]; diff --git a/modules/angular2/src/compiler/html_tags.ts b/modules/angular2/src/compiler/html_tags.ts index 07fc901a4d6e69..908871f4142d42 100644 --- a/modules/angular2/src/compiler/html_tags.ts +++ b/modules/angular2/src/compiler/html_tags.ts @@ -69,19 +69,22 @@ export class HtmlTagDefinition { public requiredParent: string; public implicitNamespacePrefix: string; public contentType: HtmlTagContentType; + public isVoid: boolean; constructor({closedByChildren, requiredParent, implicitNamespacePrefix, contentType, - closedByParent}: { + closedByParent, isVoid}: { closedByChildren?: string, closedByParent?: boolean, requiredParent?: string, implicitNamespacePrefix?: string, - contentType?: HtmlTagContentType + contentType?: HtmlTagContentType, + isVoid?: boolean } = {}) { if (isPresent(closedByChildren) && closedByChildren.length > 0) { closedByChildren.split(',').forEach(tagName => this.closedByChildren[tagName.trim()] = true); } - this.closedByParent = normalizeBool(closedByParent); + this.isVoid = normalizeBool(isVoid); + this.closedByParent = normalizeBool(closedByParent) || this.isVoid; this.requiredParent = requiredParent; this.implicitNamespacePrefix = implicitNamespacePrefix; this.contentType = isPresent(contentType) ? contentType : HtmlTagContentType.PARSABLE_DATA; @@ -93,21 +96,20 @@ export class HtmlTagDefinition { } isClosedByChild(name: string): boolean { - return normalizeBool(this.closedByChildren['*']) || - normalizeBool(this.closedByChildren[name.toLowerCase()]); + return this.isVoid || normalizeBool(this.closedByChildren[name.toLowerCase()]); } } // see http://www.w3.org/TR/html51/syntax.html#optional-tags // This implementation does not fully conform to the HTML5 spec. var TAG_DEFINITIONS: {[key: string]: HtmlTagDefinition} = { - 'link': new HtmlTagDefinition({closedByChildren: '*', closedByParent: true}), - 'ng-content': new HtmlTagDefinition({closedByChildren: '*', closedByParent: true}), - 'img': new HtmlTagDefinition({closedByChildren: '*', closedByParent: true}), - 'input': new HtmlTagDefinition({closedByChildren: '*', closedByParent: true}), - 'hr': new HtmlTagDefinition({closedByChildren: '*', closedByParent: true}), - 'br': new HtmlTagDefinition({closedByChildren: '*', closedByParent: true}), - 'wbr': new HtmlTagDefinition({closedByChildren: '*', closedByParent: true}), + 'link': new HtmlTagDefinition({isVoid: true}), + 'ng-content': new HtmlTagDefinition({isVoid: true}), + 'img': new HtmlTagDefinition({isVoid: true}), + 'input': new HtmlTagDefinition({isVoid: true}), + 'hr': new HtmlTagDefinition({isVoid: true}), + 'br': new HtmlTagDefinition({isVoid: true}), + 'wbr': new HtmlTagDefinition({isVoid: true}), 'p': new HtmlTagDefinition({ closedByChildren: 'address,article,aside,blockquote,div,dl,fieldset,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,hr,main,nav,ol,p,pre,section,table,ul', diff --git a/modules/angular2/test/compiler/html_parser_spec.ts b/modules/angular2/test/compiler/html_parser_spec.ts index 9120a05ecf4f4a..b41f46da2a9549 100644 --- a/modules/angular2/test/compiler/html_parser_spec.ts +++ b/modules/angular2/test/compiler/html_parser_spec.ts @@ -31,22 +31,22 @@ export function main() { describe('parse', () => { describe('text nodes', () => { it('should parse root level text nodes', () => { - expect(humanizeDom(parser.parse('a', 'TestComp'))).toEqual([[HtmlTextAst, 'a']]); + expect(humanizeDom(parser.parse('a', 'TestComp'))).toEqual([[HtmlTextAst, 'a', 0]]); }); it('should parse text nodes inside regular elements', () => { expect(humanizeDom(parser.parse('
a
', 'TestComp'))) - .toEqual([[HtmlElementAst, 'div', 0], [HtmlTextAst, 'a']]); + .toEqual([[HtmlElementAst, 'div', 0], [HtmlTextAst, 'a', 1]]); }); it('should parse text nodes inside template elements', () => { expect(humanizeDom(parser.parse('', 'TestComp'))) - .toEqual([[HtmlElementAst, 'template', 0], [HtmlTextAst, 'a']]); + .toEqual([[HtmlElementAst, 'template', 0], [HtmlTextAst, 'a', 1]]); }); it('should parse CDATA', () => { expect(humanizeDom(parser.parse('', 'TestComp'))) - .toEqual([[HtmlTextAst, 'text']]); + .toEqual([[HtmlTextAst, 'text', 0]]); }); }); @@ -75,14 +75,24 @@ export function main() { ]); }); + it('should close void elements on text nodes', () => { + expect(humanizeDom(parser.parse('

before
after

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

1

2

', 'TestComp'))) .toEqual([ [HtmlElementAst, 'div', 0], [HtmlElementAst, 'p', 1], - [HtmlTextAst, '1'], + [HtmlTextAst, '1', 2], [HtmlElementAst, 'p', 1], - [HtmlTextAst, '2'], + [HtmlTextAst, '2', 2], ]); }); @@ -174,7 +184,7 @@ export function main() { [HtmlAttrAst, '(e)', 'do()', '(e)="do()"'], [HtmlAttrAst, 'attr', 'v2', 'attr="v2"'], [HtmlAttrAst, 'noValue', '', 'noValue'], - [HtmlTextAst, '\na\n', '\na\n'], + [HtmlTextAst, '\na\n', 1, '\na\n'], ]); }); }); @@ -259,7 +269,7 @@ class Humanizer implements HtmlAstVisitor { } visitText(ast: HtmlTextAst, context: any): any { - var res = this._appendContext(ast, [HtmlTextAst, ast.value]); + var res = this._appendContext(ast, [HtmlTextAst, ast.value, this.elDepth]); this.result.push(res); return null; }