Skip to content

Commit

Permalink
fix(HtmlParser): close void elements on all node types
Browse files Browse the repository at this point in the history
fixes #5528
  • Loading branch information
vicb authored and jelbourn committed Dec 2, 2015
1 parent 070d818 commit 9c6b929
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 20 deletions.
13 changes: 13 additions & 0 deletions modules/angular2/src/compiler/html_parser.ts
Expand Up @@ -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...
Expand Down Expand Up @@ -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];
Expand Down
26 changes: 14 additions & 12 deletions modules/angular2/src/compiler/html_tags.ts
Expand Up @@ -70,19 +70,22 @@ export class HtmlTagDefinition {
public parentToAdd: string;
public implicitNamespacePrefix: string;
public contentType: HtmlTagContentType;
public isVoid: boolean;

constructor({closedByChildren, requiredParents, implicitNamespacePrefix, contentType,
closedByParent}: {
closedByParent, isVoid}: {
closedByChildren?: string[],
closedByParent?: boolean,
requiredParents?: string[],
implicitNamespacePrefix?: string,
contentType?: HtmlTagContentType
contentType?: HtmlTagContentType,
isVoid?: boolean
} = {}) {
if (isPresent(closedByChildren) && closedByChildren.length > 0) {
closedByChildren.forEach(tagName => this.closedByChildren[tagName] = true);
}
this.closedByParent = normalizeBool(closedByParent);
this.isVoid = normalizeBool(isVoid);
this.closedByParent = normalizeBool(closedByParent) || this.isVoid;
if (isPresent(requiredParents) && requiredParents.length > 0) {
this.requiredParents = {};
this.parentToAdd = requiredParents[0];
Expand All @@ -98,21 +101,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',
Expand Down
26 changes: 18 additions & 8 deletions modules/angular2/test/compiler/html_parser_spec.ts
Expand Up @@ -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('<div>a</div>', '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('<template>a</template>', 'TestComp')))
.toEqual([[HtmlElementAst, 'template', 0], [HtmlTextAst, 'a']]);
.toEqual([[HtmlElementAst, 'template', 0], [HtmlTextAst, 'a', 1]]);
});

it('should parse CDATA', () => {
expect(humanizeDom(parser.parse('<![CDATA[text]]>', 'TestComp')))
.toEqual([[HtmlTextAst, 'text']]);
.toEqual([[HtmlTextAst, 'text', 0]]);
});
});

Expand Down Expand Up @@ -75,14 +75,24 @@ export function main() {
]);
});

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],
]);
});

it('should support optional end tags', () => {
expect(humanizeDom(parser.parse('<div><p>1<p>2</div>', 'TestComp')))
.toEqual([
[HtmlElementAst, 'div', 0],
[HtmlElementAst, 'p', 1],
[HtmlTextAst, '1'],
[HtmlTextAst, '1', 2],
[HtmlElementAst, 'p', 1],
[HtmlTextAst, '2'],
[HtmlTextAst, '2', 2],
]);
});

Expand Down Expand Up @@ -187,7 +197,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'],
]);
});
});
Expand Down Expand Up @@ -272,7 +282,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;
}
Expand Down

0 comments on commit 9c6b929

Please sign in to comment.