/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {TagContentType, TagDefinition} from './tags'; export class HtmlTagDefinition implements TagDefinition { private closedByChildren: {[key: string]: boolean} = {}; closedByParent: boolean = false; // TODO(issue/24571): remove '!'. requiredParents !: {[key: string]: boolean}; // TODO(issue/24571): remove '!'. parentToAdd !: string; implicitNamespacePrefix: string|null; contentType: TagContentType; isVoid: boolean; ignoreFirstLf: boolean; canSelfClose: boolean = false; constructor( {closedByChildren, requiredParents, implicitNamespacePrefix, contentType = TagContentType.PARSABLE_DATA, closedByParent = false, isVoid = false, ignoreFirstLf = false}: { closedByChildren?: string[], closedByParent?: boolean, requiredParents?: string[], implicitNamespacePrefix?: string, contentType?: TagContentType, isVoid?: boolean, ignoreFirstLf?: boolean } = {}) { if (closedByChildren && closedByChildren.length > 0) { closedByChildren.forEach(tagName => this.closedByChildren[tagName] = true); } this.isVoid = isVoid; this.closedByParent = closedByParent || isVoid; if (requiredParents && requiredParents.length > 0) { this.requiredParents = {}; // The first parent is the list is automatically when none of the listed parents are present this.parentToAdd = requiredParents[0]; requiredParents.forEach(tagName => this.requiredParents[tagName] = true); } this.implicitNamespacePrefix = implicitNamespacePrefix || null; this.contentType = contentType; this.ignoreFirstLf = ignoreFirstLf; } requireExtraParent(currentParent: string): boolean { if (!this.requiredParents) { return false; } if (!currentParent) { return true; } const lcParent = currentParent.toLowerCase(); const isParentTemplate = lcParent === 'template' || currentParent === 'ng-template'; return !isParentTemplate && this.requiredParents[lcParent] != true; } isClosedByChild(name: string): boolean { return this.isVoid || name.toLowerCase() in this.closedByChildren; } } // see http://www.w3.org/TR/html51/syntax.html#optional-tags // This implementation does not fully conform to the HTML5 spec. const TAG_DEFINITIONS: {[key: string]: HtmlTagDefinition} = { 'base': new HtmlTagDefinition({isVoid: true}), 'meta': new HtmlTagDefinition({isVoid: true}), 'area': new HtmlTagDefinition({isVoid: true}), 'embed': new HtmlTagDefinition({isVoid: true}), 'link': new HtmlTagDefinition({isVoid: true}), 'img': new HtmlTagDefinition({isVoid: true}), 'input': new HtmlTagDefinition({isVoid: true}), 'param': new HtmlTagDefinition({isVoid: true}), 'hr': new HtmlTagDefinition({isVoid: true}), 'br': new HtmlTagDefinition({isVoid: true}), 'source': new HtmlTagDefinition({isVoid: true}), 'track': 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' ], closedByParent: true }), 'thead': new HtmlTagDefinition({closedByChildren: ['tbody', 'tfoot']}), 'tbody': new HtmlTagDefinition({closedByChildren: ['tbody', 'tfoot'], closedByParent: true}), 'tfoot': new HtmlTagDefinition({closedByChildren: ['tbody'], closedByParent: true}), 'tr': new HtmlTagDefinition({ closedByChildren: ['tr'], requiredParents: ['tbody', 'tfoot', 'thead'], closedByParent: true }), 'td': new HtmlTagDefinition({closedByChildren: ['td', 'th'], closedByParent: true}), 'th': new HtmlTagDefinition({closedByChildren: ['td', 'th'], closedByParent: true}), 'col': new HtmlTagDefinition({requiredParents: ['colgroup'], isVoid: true}), 'svg': new HtmlTagDefinition({implicitNamespacePrefix: 'svg'}), 'math': new HtmlTagDefinition({implicitNamespacePrefix: 'math'}), 'li': new HtmlTagDefinition({closedByChildren: ['li'], closedByParent: true}), 'dt': new HtmlTagDefinition({closedByChildren: ['dt', 'dd']}), 'dd': new HtmlTagDefinition({closedByChildren: ['dt', 'dd'], closedByParent: true}), 'rb': 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}), 'rp': new HtmlTagDefinition({closedByChildren: ['rb', 'rt', 'rtc', 'rp'], closedByParent: true}), 'optgroup': new HtmlTagDefinition({closedByChildren: ['optgroup'], closedByParent: true}), 'option': new HtmlTagDefinition({closedByChildren: ['option', 'optgroup'], closedByParent: true}), 'pre': new HtmlTagDefinition({ignoreFirstLf: true}), 'listing': new HtmlTagDefinition({ignoreFirstLf: true}), 'style': new HtmlTagDefinition({contentType: TagContentType.RAW_TEXT}), 'script': new HtmlTagDefinition({contentType: TagContentType.RAW_TEXT}), 'title': new HtmlTagDefinition({contentType: TagContentType.ESCAPABLE_RAW_TEXT}), 'textarea': new HtmlTagDefinition({contentType: TagContentType.ESCAPABLE_RAW_TEXT, ignoreFirstLf: true}), }; const _DEFAULT_TAG_DEFINITION = new HtmlTagDefinition(); export function getHtmlTagDefinition(tagName: string): HtmlTagDefinition { return TAG_DEFINITIONS[tagName.toLowerCase()] || _DEFAULT_TAG_DEFINITION; }