fix(HtmlParser): do not add a tbody parent for tr inside thead & tfoot

fixes #5403
This commit is contained in:
Victor Berchet 2015-11-20 13:13:48 -08:00 committed by Jeremy Elbourn
parent bdfed9d850
commit c58e7e0e91
3 changed files with 83 additions and 36 deletions

View File

@ -145,7 +145,7 @@ class TreeBuilder {
var tagDef = getHtmlTagDefinition(el.name); var tagDef = getHtmlTagDefinition(el.name);
var parentEl = this._getParentElement(); var parentEl = this._getParentElement();
if (tagDef.requireExtraParent(isPresent(parentEl) ? parentEl.name : null)) { if (tagDef.requireExtraParent(isPresent(parentEl) ? parentEl.name : null)) {
var newParent = new HtmlElementAst(tagDef.requiredParent, [], [el], el.sourceSpan); var newParent = new HtmlElementAst(tagDef.parentToAdd, [], [el], el.sourceSpan);
this._addToParent(newParent); this._addToParent(newParent);
this.elementStack.push(newParent); this.elementStack.push(newParent);
this.elementStack.push(el); this.elementStack.push(el);

View File

@ -66,30 +66,35 @@ export enum HtmlTagContentType {
export class HtmlTagDefinition { export class HtmlTagDefinition {
private closedByChildren: {[key: string]: boolean} = {}; private closedByChildren: {[key: string]: boolean} = {};
public closedByParent: boolean = false; public closedByParent: boolean = false;
public requiredParent: string; public requiredParents: {[key: string]: boolean};
public parentToAdd: string;
public implicitNamespacePrefix: string; public implicitNamespacePrefix: string;
public contentType: HtmlTagContentType; public contentType: HtmlTagContentType;
constructor({closedByChildren, requiredParent, implicitNamespacePrefix, contentType, constructor({closedByChildren, requiredParents, implicitNamespacePrefix, contentType,
closedByParent}: { closedByParent}: {
closedByChildren?: string, closedByChildren?: string[],
closedByParent?: boolean, closedByParent?: boolean,
requiredParent?: string, requiredParents?: string[],
implicitNamespacePrefix?: string, implicitNamespacePrefix?: string,
contentType?: HtmlTagContentType contentType?: HtmlTagContentType
} = {}) { } = {}) {
if (isPresent(closedByChildren) && closedByChildren.length > 0) { if (isPresent(closedByChildren) && closedByChildren.length > 0) {
closedByChildren.split(',').forEach(tagName => this.closedByChildren[tagName.trim()] = true); closedByChildren.forEach(tagName => this.closedByChildren[tagName] = true);
} }
this.closedByParent = normalizeBool(closedByParent); this.closedByParent = normalizeBool(closedByParent);
this.requiredParent = requiredParent; if (isPresent(requiredParents) && requiredParents.length > 0) {
this.requiredParents = {};
this.parentToAdd = requiredParents[0];
requiredParents.forEach(tagName => this.requiredParents[tagName] = true);
}
this.implicitNamespacePrefix = implicitNamespacePrefix; this.implicitNamespacePrefix = implicitNamespacePrefix;
this.contentType = isPresent(contentType) ? contentType : HtmlTagContentType.PARSABLE_DATA; this.contentType = isPresent(contentType) ? contentType : HtmlTagContentType.PARSABLE_DATA;
} }
requireExtraParent(currentParent: string): boolean { requireExtraParent(currentParent: string): boolean {
return isPresent(this.requiredParent) && return isPresent(this.requiredParents) &&
(isBlank(currentParent) || this.requiredParent != currentParent.toLowerCase()); (isBlank(currentParent) || this.requiredParents[currentParent.toLowerCase()] != true);
} }
isClosedByChild(name: string): boolean { isClosedByChild(name: string): boolean {
@ -101,37 +106,66 @@ export class HtmlTagDefinition {
// 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({closedByChildren: ['*'], closedByParent: true}),
'ng-content': new HtmlTagDefinition({closedByChildren: '*', closedByParent: true}), 'ng-content': new HtmlTagDefinition({closedByChildren: ['*'], closedByParent: true}),
'img': new HtmlTagDefinition({closedByChildren: '*', closedByParent: true}), 'img': new HtmlTagDefinition({closedByChildren: ['*'], closedByParent: true}),
'input': new HtmlTagDefinition({closedByChildren: '*', closedByParent: true}), 'input': new HtmlTagDefinition({closedByChildren: ['*'], closedByParent: true}),
'hr': new HtmlTagDefinition({closedByChildren: '*', closedByParent: true}), 'hr': new HtmlTagDefinition({closedByChildren: ['*'], closedByParent: true}),
'br': new HtmlTagDefinition({closedByChildren: '*', closedByParent: true}), 'br': new HtmlTagDefinition({closedByChildren: ['*'], closedByParent: true}),
'wbr': new HtmlTagDefinition({closedByChildren: '*', closedByParent: true}), 'wbr': new HtmlTagDefinition({closedByChildren: ['*'], closedByParent: true}),
'p': new HtmlTagDefinition({ 'p': new HtmlTagDefinition({
closedByChildren: 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', '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'
],
closedByParent: true closedByParent: true
}), }),
'thead': new HtmlTagDefinition({closedByChildren: 'tbody,tfoot'}), 'thead': new HtmlTagDefinition({closedByChildren: ['tbody', 'tfoot']}),
'tbody': new HtmlTagDefinition({closedByChildren: 'tbody,tfoot', closedByParent: true}), 'tbody': new HtmlTagDefinition({closedByChildren: ['tbody', 'tfoot'], closedByParent: true}),
'tfoot': new HtmlTagDefinition({closedByChildren: 'tbody', closedByParent: true}), 'tfoot': new HtmlTagDefinition({closedByChildren: ['tbody'], closedByParent: true}),
'tr': new HtmlTagDefinition( 'tr': new HtmlTagDefinition({
{closedByChildren: 'tr', requiredParent: 'tbody', closedByParent: true}), closedByChildren: ['tr'],
'td': new HtmlTagDefinition({closedByChildren: 'td,th', closedByParent: true}), requiredParents: ['tbody', 'tfoot', 'thead'],
'th': new HtmlTagDefinition({closedByChildren: 'td,th', closedByParent: true}), closedByParent: true
'col': new HtmlTagDefinition({closedByChildren: 'col', requiredParent: 'colgroup'}), }),
'td': new HtmlTagDefinition({closedByChildren: ['td', 'th'], closedByParent: true}),
'th': new HtmlTagDefinition({closedByChildren: ['td', 'th'], closedByParent: true}),
'col': new HtmlTagDefinition({closedByChildren: ['col'], requiredParents: ['colgroup']}),
'svg': new HtmlTagDefinition({implicitNamespacePrefix: 'svg'}), 'svg': new HtmlTagDefinition({implicitNamespacePrefix: 'svg'}),
'math': new HtmlTagDefinition({implicitNamespacePrefix: 'math'}), 'math': new HtmlTagDefinition({implicitNamespacePrefix: 'math'}),
'li': new HtmlTagDefinition({closedByChildren: 'li', closedByParent: true}), 'li': new HtmlTagDefinition({closedByChildren: ['li'], closedByParent: true}),
'dt': new HtmlTagDefinition({closedByChildren: 'dt,dd'}), 'dt': new HtmlTagDefinition({closedByChildren: ['dt', 'dd']}),
'dd': new HtmlTagDefinition({closedByChildren: 'dt,dd', closedByParent: true}), 'dd': new HtmlTagDefinition({closedByChildren: ['dt', 'dd'], closedByParent: true}),
'rb': new HtmlTagDefinition({closedByChildren: 'rb,rt,rtc,rp', closedByParent: true}), 'rb': new HtmlTagDefinition({closedByChildren: ['rb', 'rt', 'rtc', 'rp'], closedByParent: true}),
'rt': new HtmlTagDefinition({closedByChildren: 'rb,rt,rtc,rp', closedByParent: true}), 'rt': new HtmlTagDefinition({closedByChildren: ['rb', 'rt', 'rtc', 'rp'], closedByParent: true}),
'rtc': new HtmlTagDefinition({closedByChildren: 'rb,rtc,rp', closedByParent: true}), 'rtc': new HtmlTagDefinition({closedByChildren: ['rb', 'rtc', 'rp'], closedByParent: true}),
'rp': new HtmlTagDefinition({closedByChildren: 'rb,rt,rtc,rp', closedByParent: true}), 'rp': new HtmlTagDefinition({closedByChildren: ['rb', 'rt', 'rtc', 'rp'], closedByParent: true}),
'optgroup': new HtmlTagDefinition({closedByChildren: 'optgroup', closedByParent: true}), 'optgroup': new HtmlTagDefinition({closedByChildren: ['optgroup'], closedByParent: true}),
'option': new HtmlTagDefinition({closedByChildren: 'option,optgroup', closedByParent: true}), 'option': new HtmlTagDefinition({closedByChildren: ['option', 'optgroup'], closedByParent: true}),
'style': new HtmlTagDefinition({contentType: HtmlTagContentType.RAW_TEXT}), 'style': new HtmlTagDefinition({contentType: HtmlTagContentType.RAW_TEXT}),
'script': new HtmlTagDefinition({contentType: HtmlTagContentType.RAW_TEXT}), 'script': new HtmlTagDefinition({contentType: HtmlTagContentType.RAW_TEXT}),
'title': new HtmlTagDefinition({contentType: HtmlTagContentType.ESCAPABLE_RAW_TEXT}), 'title': new HtmlTagDefinition({contentType: HtmlTagContentType.ESCAPABLE_RAW_TEXT}),

View File

@ -97,11 +97,24 @@ export function main() {
}); });
it('should add the requiredParent', () => { it('should add the requiredParent', () => {
expect(humanizeDom(parser.parse('<table><tr></tr></table>', 'TestComp'))) 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([ .toEqual([
[HtmlElementAst, 'table', 0], [HtmlElementAst, 'table', 0],
[HtmlElementAst, 'thead', 1],
[HtmlElementAst, 'tr', 2],
[HtmlAttrAst, 'head', ''],
[HtmlElementAst, 'tbody', 1], [HtmlElementAst, 'tbody', 1],
[HtmlElementAst, 'tr', 2], [HtmlElementAst, 'tr', 2],
[HtmlAttrAst, 'noparent', ''],
[HtmlElementAst, 'tbody', 1],
[HtmlElementAst, 'tr', 2],
[HtmlAttrAst, 'body', ''],
[HtmlElementAst, 'tfoot', 1],
[HtmlElementAst, 'tr', 2],
[HtmlAttrAst, 'foot', '']
]); ]);
}); });