eslint-plugin-validate-jsx-nesting
Version:
ESLint Plugin for Validating JSX Nesting
265 lines (248 loc) • 5.95 kB
JavaScript
'use strict';
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$1 = {
head: new Set([
'base',
'basefront',
'bgsound',
'link',
'meta',
'title',
'noscript',
'noframes',
'style',
'script',
'template',
]),
optgroup: new Set(['option']),
select: new Set(['optgroup', 'option']),
math: new Set(['mrow']),
script: new Set(),
// 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
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$1 = {
// 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']),
};
/** maps element to set of elements that can not be it's children, others can */
const knownInvalidChildren$1 = {
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',
]),
};
/** maps element to set of elements that can not be it's parent, others can */
const knownInvalidParents$1 = {
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,
};
var mapping = {
onlyValidChildren: onlyValidChildren$1,
onlyValidParents: onlyValidParents$1,
knownInvalidChildren: knownInvalidChildren$1,
knownInvalidParents: knownInvalidParents$1,
};
const {
onlyValidChildren,
onlyValidParents,
knownInvalidChildren,
knownInvalidParents,
} = mapping;
/**
* returns true if given parent-child nesting is valid HTML
* @param {string} child
* @param {string} parent
* @returns {boolean}
*/
function isValidHTMLNesting(parent, child) {
// 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;
}
var src = {
isValidHTMLNesting,
};
/** If the name starts with uppercase, it's a component name */
function isCompName(str) {
return str[0] === str[0].toUpperCase();
}
/** node is JSXElement if it's type is JSXElement */
function isJSXElement(node) {
return typeof node === 'object' && node !== null && node.type === 'JSXElement';
}
const noInvalidJSXNesting = {
meta: {
type: 'problem',
},
create(context) {
return {
JSXElement(node) {
// get node and it's parent
const jsxElement = node;
const parent = node.parent;
// return if node is not a native element
const elName = jsxElement.openingElement.name;
if (elName.type !== 'JSXIdentifier')
return;
if (isCompName(elName.name))
return;
// return if parent is not a native element
if (!isJSXElement(parent))
return;
const parentElName = parent.openingElement.name;
if (parentElName.type !== 'JSXIdentifier')
return;
if (isCompName(parentElName.name))
return;
// if both are native elements, check if the nesting is valid
// return if nesting is valid
if (src.isValidHTMLNesting(parentElName.name, elName.name))
return;
// report error if nesting is invalid
context.report({
node,
message: `Invalid HTML nesting: <${elName.name}> can not be child of <${parentElName.name}>`,
});
},
};
},
};
var index = {
rules: {
'no-invalid-jsx-nesting': noInvalidJSXNesting,
},
};
module.exports = index;