/** * Copied from https://github.com/MananTank/validate-html-nesting * with ISC license * * To avoid runtime dependency on validate-html-nesting * This file should not change very often in the original repo * but we may need to keep it up-to-date from time to time. */ /** * returns true if given parent-child nesting is valid HTML */ export function isValidHTMLNesting(parent: string, child: string): boolean { // if the parent is a template, it can have any child if (parent === 'template') { return true } // if we know the list of children that are the only valid children for the given parent if (parent in onlyValidChildren) { return onlyValidChildren[parent].has(child) } // if we know the list of parents that are the only valid parents for the given child if (child in onlyValidParents) { return onlyValidParents[child].has(parent) } // if we know the list of children that are NOT valid for the given parent if (parent in knownInvalidChildren) { // check if the child is in the list of invalid children // if so, return false if (knownInvalidChildren[parent].has(child)) return false } // if we know the list of parents that are NOT valid for the given child if (child in knownInvalidParents) { // check if the parent is in the list of invalid parents // if so, return false if (knownInvalidParents[child].has(parent)) return false } return true } const headings = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']) const emptySet = new Set([]) /** * maps element to set of elements that can be it's children, no other */ const onlyValidChildren: Record> = { head: new Set([ 'base', 'basefront', 'bgsound', 'link', 'meta', 'title', 'noscript', 'noframes', 'style', 'script', 'template', ]), optgroup: new Set(['option']), select: new Set(['optgroup', 'option', 'hr']), // table table: new Set(['caption', 'colgroup', 'tbody', 'tfoot', 'thead']), tr: new Set(['td', 'th']), colgroup: new Set(['col']), tbody: new Set(['tr']), thead: new Set(['tr']), tfoot: new Set(['tr']), // these elements can not have any children elements script: emptySet, iframe: emptySet, option: emptySet, textarea: emptySet, style: emptySet, title: emptySet, } /** maps elements to set of elements which can be it's parent, no other */ const onlyValidParents: Record> = { // sections html: emptySet, body: new Set(['html']), head: new Set(['html']), // table td: new Set(['tr']), colgroup: new Set(['table']), caption: new Set(['table']), tbody: new Set(['table']), tfoot: new Set(['table']), col: new Set(['colgroup']), th: new Set(['tr']), thead: new Set(['table']), tr: new Set(['tbody', 'thead', 'tfoot']), // data list dd: new Set(['dl', 'div']), dt: new Set(['dl', 'div']), // other figcaption: new Set(['figure']), // li: new Set(["ul", "ol"]), summary: new Set(['details']), area: new Set(['map']), } as const /** maps element to set of elements that can not be it's children, others can */ const knownInvalidChildren: Record> = { p: new Set([ 'address', 'article', 'aside', 'blockquote', 'center', 'details', 'dialog', 'dir', 'div', 'dl', 'fieldset', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'li', 'main', 'nav', 'menu', 'ol', 'p', 'pre', 'section', 'table', 'ul', ]), svg: new Set([ 'b', 'blockquote', 'br', 'code', 'dd', 'div', 'dl', 'dt', 'em', 'embed', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'li', 'menu', 'meta', 'ol', 'p', 'pre', 'ruby', 's', 'small', 'span', 'strong', 'sub', 'sup', 'table', 'u', 'ul', 'var', ]), } as const /** maps element to set of elements that can not be it's parent, others can */ const knownInvalidParents: Record> = { a: new Set(['a']), button: new Set(['button']), dd: new Set(['dd', 'dt']), dt: new Set(['dd', 'dt']), form: new Set(['form']), li: new Set(['li']), h1: headings, h2: headings, h3: headings, h4: headings, h5: headings, h6: headings, }