fix(HtmlParser): close void elements on all node types

fixes #5528
This commit is contained in:
Victor Berchet 2015-12-01 13:01:05 -08:00 committed by Jeremy Elbourn
parent 070d818e68
commit 9c6b929c7b
3 changed files with 45 additions and 20 deletions

View File

@ -61,12 +61,15 @@ class TreeBuilder {
} else if (this.peek.type === HtmlTokenType.TAG_CLOSE) { } else if (this.peek.type === HtmlTokenType.TAG_CLOSE) {
this._consumeEndTag(this._advance()); this._consumeEndTag(this._advance());
} else if (this.peek.type === HtmlTokenType.CDATA_START) { } else if (this.peek.type === HtmlTokenType.CDATA_START) {
this._closeVoidElement();
this._consumeCdata(this._advance()); this._consumeCdata(this._advance());
} else if (this.peek.type === HtmlTokenType.COMMENT_START) { } else if (this.peek.type === HtmlTokenType.COMMENT_START) {
this._closeVoidElement();
this._consumeComment(this._advance()); this._consumeComment(this._advance());
} else if (this.peek.type === HtmlTokenType.TEXT || } else if (this.peek.type === HtmlTokenType.TEXT ||
this.peek.type === HtmlTokenType.RAW_TEXT || this.peek.type === HtmlTokenType.RAW_TEXT ||
this.peek.type === HtmlTokenType.ESCAPABLE_RAW_TEXT) { this.peek.type === HtmlTokenType.ESCAPABLE_RAW_TEXT) {
this._closeVoidElement();
this._consumeText(this._advance()); this._consumeText(this._advance());
} else { } else {
// Skip all other tokens... // Skip all other tokens...
@ -107,6 +110,16 @@ class TreeBuilder {
this._addToParent(new HtmlTextAst(token.parts[0], token.sourceSpan)); 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) { private _consumeStartTag(startTagToken: HtmlToken) {
var prefix = startTagToken.parts[0]; var prefix = startTagToken.parts[0];
var name = startTagToken.parts[1]; var name = startTagToken.parts[1];

View File

@ -70,19 +70,22 @@ export class HtmlTagDefinition {
public parentToAdd: string; public parentToAdd: string;
public implicitNamespacePrefix: string; public implicitNamespacePrefix: string;
public contentType: HtmlTagContentType; public contentType: HtmlTagContentType;
public isVoid: boolean;
constructor({closedByChildren, requiredParents, implicitNamespacePrefix, contentType, constructor({closedByChildren, requiredParents, implicitNamespacePrefix, contentType,
closedByParent}: { closedByParent, isVoid}: {
closedByChildren?: string[], closedByChildren?: string[],
closedByParent?: boolean, closedByParent?: boolean,
requiredParents?: string[], requiredParents?: string[],
implicitNamespacePrefix?: string, implicitNamespacePrefix?: string,
contentType?: HtmlTagContentType contentType?: HtmlTagContentType,
isVoid?: boolean
} = {}) { } = {}) {
if (isPresent(closedByChildren) && closedByChildren.length > 0) { if (isPresent(closedByChildren) && closedByChildren.length > 0) {
closedByChildren.forEach(tagName => this.closedByChildren[tagName] = true); 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) { if (isPresent(requiredParents) && requiredParents.length > 0) {
this.requiredParents = {}; this.requiredParents = {};
this.parentToAdd = requiredParents[0]; this.parentToAdd = requiredParents[0];
@ -98,21 +101,20 @@ export class HtmlTagDefinition {
} }
isClosedByChild(name: string): boolean { isClosedByChild(name: string): boolean {
return normalizeBool(this.closedByChildren['*']) || return this.isVoid || normalizeBool(this.closedByChildren[name.toLowerCase()]);
normalizeBool(this.closedByChildren[name.toLowerCase()]);
} }
} }
// see http://www.w3.org/TR/html51/syntax.html#optional-tags // see http://www.w3.org/TR/html51/syntax.html#optional-tags
// This implementation does not fully conform to the HTML5 spec. // This implementation does not fully conform to the HTML5 spec.
var TAG_DEFINITIONS: {[key: string]: HtmlTagDefinition} = { var TAG_DEFINITIONS: {[key: string]: HtmlTagDefinition} = {
'link': new HtmlTagDefinition({closedByChildren: ['*'], closedByParent: true}), 'link': new HtmlTagDefinition({isVoid: true}),
'ng-content': new HtmlTagDefinition({closedByChildren: ['*'], closedByParent: true}), 'ng-content': new HtmlTagDefinition({isVoid: true}),
'img': new HtmlTagDefinition({closedByChildren: ['*'], closedByParent: true}), 'img': new HtmlTagDefinition({isVoid: true}),
'input': new HtmlTagDefinition({closedByChildren: ['*'], closedByParent: true}), 'input': new HtmlTagDefinition({isVoid: true}),
'hr': new HtmlTagDefinition({closedByChildren: ['*'], closedByParent: true}), 'hr': new HtmlTagDefinition({isVoid: true}),
'br': new HtmlTagDefinition({closedByChildren: ['*'], closedByParent: true}), 'br': new HtmlTagDefinition({isVoid: true}),
'wbr': new HtmlTagDefinition({closedByChildren: ['*'], closedByParent: true}), 'wbr': new HtmlTagDefinition({isVoid: true}),
'p': new HtmlTagDefinition({ 'p': new HtmlTagDefinition({
closedByChildren: [ closedByChildren: [
'address', 'address',

View File

@ -31,22 +31,22 @@ export function main() {
describe('parse', () => { describe('parse', () => {
describe('text nodes', () => { describe('text nodes', () => {
it('should parse root level 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', () => { it('should parse text nodes inside regular elements', () => {
expect(humanizeDom(parser.parse('<div>a</div>', 'TestComp'))) 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', () => { it('should parse text nodes inside template elements', () => {
expect(humanizeDom(parser.parse('<template>a</template>', 'TestComp'))) 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', () => { it('should parse CDATA', () => {
expect(humanizeDom(parser.parse('<![CDATA[text]]>', 'TestComp'))) expect(humanizeDom(parser.parse('<![CDATA[text]]>', '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('<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', () => { it('should support optional end tags', () => {
expect(humanizeDom(parser.parse('<div><p>1<p>2</div>', 'TestComp'))) expect(humanizeDom(parser.parse('<div><p>1<p>2</div>', 'TestComp')))
.toEqual([ .toEqual([
[HtmlElementAst, 'div', 0], [HtmlElementAst, 'div', 0],
[HtmlElementAst, 'p', 1], [HtmlElementAst, 'p', 1],
[HtmlTextAst, '1'], [HtmlTextAst, '1', 2],
[HtmlElementAst, 'p', 1], [HtmlElementAst, 'p', 1],
[HtmlTextAst, '2'], [HtmlTextAst, '2', 2],
]); ]);
}); });
@ -187,7 +197,7 @@ export function main() {
[HtmlAttrAst, '(e)', 'do()', '(e)="do()"'], [HtmlAttrAst, '(e)', 'do()', '(e)="do()"'],
[HtmlAttrAst, 'attr', 'v2', 'attr="v2"'], [HtmlAttrAst, 'attr', 'v2', 'attr="v2"'],
[HtmlAttrAst, 'noValue', '', 'noValue'], [HtmlAttrAst, 'noValue', '', 'noValue'],
[HtmlTextAst, '\na\n', '\na\n'], [HtmlTextAst, '\na\n', 1, '\na\n'],
]); ]);
}); });
}); });
@ -272,7 +282,7 @@ class Humanizer implements HtmlAstVisitor {
} }
visitText(ast: HtmlTextAst, context: any): any { 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); this.result.push(res);
return null; return null;
} }