import {getCurrentParentStackInDev} from 'react-reconciler/src/ReactCurrentFiber';
type Info = {tag: string};
export type AncestorInfoDev = {
current: ?Info,
formTag: ?Info,
aTagInScope: ?Info,
buttonTagInScope: ?Info,
nobrTagInScope: ?Info,
pTagInButtonScope: ?Info,
listItemTagAutoclosing: ?Info,
dlItemTagAutoclosing: ?Info,
containerTagInScope: ?Info,
};
const specialTags = [
'address',
'applet',
'area',
'article',
'aside',
'base',
'basefont',
'bgsound',
'blockquote',
'body',
'br',
'button',
'caption',
'center',
'col',
'colgroup',
'dd',
'details',
'dir',
'div',
'dl',
'dt',
'embed',
'fieldset',
'figcaption',
'figure',
'footer',
'form',
'frame',
'frameset',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'head',
'header',
'hgroup',
'hr',
'html',
'iframe',
'img',
'input',
'isindex',
'li',
'link',
'listing',
'main',
'marquee',
'menu',
'menuitem',
'meta',
'nav',
'noembed',
'noframes',
'noscript',
'object',
'ol',
'p',
'param',
'plaintext',
'pre',
'script',
'section',
'select',
'source',
'style',
'summary',
'table',
'tbody',
'td',
'template',
'textarea',
'tfoot',
'th',
'thead',
'title',
'tr',
'track',
'ul',
'wbr',
'xmp',
];
const inScopeTags = [
'applet',
'caption',
'html',
'table',
'td',
'th',
'marquee',
'object',
'template',
'foreignObject',
'desc',
'title',
];
const buttonScopeTags = __DEV__ ? inScopeTags.concat(['button']) : [];
const impliedEndTags = [
'dd',
'dt',
'li',
'option',
'optgroup',
'p',
'rp',
'rt',
];
const emptyAncestorInfoDev: AncestorInfoDev = {
current: null,
formTag: null,
aTagInScope: null,
buttonTagInScope: null,
nobrTagInScope: null,
pTagInButtonScope: null,
listItemTagAutoclosing: null,
dlItemTagAutoclosing: null,
containerTagInScope: null,
};
function updatedAncestorInfoDev(
oldInfo: ?AncestorInfoDev,
tag: string,
): AncestorInfoDev {
if (__DEV__) {
const ancestorInfo = {...(oldInfo || emptyAncestorInfoDev)};
const info = {tag};
if (inScopeTags.indexOf(tag) !== -1) {
ancestorInfo.aTagInScope = null;
ancestorInfo.buttonTagInScope = null;
ancestorInfo.nobrTagInScope = null;
}
if (buttonScopeTags.indexOf(tag) !== -1) {
ancestorInfo.pTagInButtonScope = null;
}
if (
specialTags.indexOf(tag) !== -1 &&
tag !== 'address' &&
tag !== 'div' &&
tag !== 'p'
) {
ancestorInfo.listItemTagAutoclosing = null;
ancestorInfo.dlItemTagAutoclosing = null;
}
ancestorInfo.current = info;
if (tag === 'form') {
ancestorInfo.formTag = info;
}
if (tag === 'a') {
ancestorInfo.aTagInScope = info;
}
if (tag === 'button') {
ancestorInfo.buttonTagInScope = info;
}
if (tag === 'nobr') {
ancestorInfo.nobrTagInScope = info;
}
if (tag === 'p') {
ancestorInfo.pTagInButtonScope = info;
}
if (tag === 'li') {
ancestorInfo.listItemTagAutoclosing = info;
}
if (tag === 'dd' || tag === 'dt') {
ancestorInfo.dlItemTagAutoclosing = info;
}
if (tag === '#document' || tag === 'html') {
ancestorInfo.containerTagInScope = null;
} else if (!ancestorInfo.containerTagInScope) {
ancestorInfo.containerTagInScope = info;
}
return ancestorInfo;
} else {
return (null: any);
}
}
function isTagValidWithParent(tag: string, parentTag: ?string): boolean {
switch (parentTag) {
case 'select':
return (
tag === 'hr' ||
tag === 'option' ||
tag === 'optgroup' ||
tag === '#text'
);
case 'optgroup':
return tag === 'option' || tag === '#text';
case 'option':
return tag === '#text';
case 'tr':
return (
tag === 'th' ||
tag === 'td' ||
tag === 'style' ||
tag === 'script' ||
tag === 'template'
);
case 'tbody':
case 'thead':
case 'tfoot':
return (
tag === 'tr' ||
tag === 'style' ||
tag === 'script' ||
tag === 'template'
);
case 'colgroup':
return tag === 'col' || tag === 'template';
case 'table':
return (
tag === 'caption' ||
tag === 'colgroup' ||
tag === 'tbody' ||
tag === 'tfoot' ||
tag === 'thead' ||
tag === 'style' ||
tag === 'script' ||
tag === 'template'
);
case 'head':
return (
tag === 'base' ||
tag === 'basefont' ||
tag === 'bgsound' ||
tag === 'link' ||
tag === 'meta' ||
tag === 'title' ||
tag === 'noscript' ||
tag === 'noframes' ||
tag === 'style' ||
tag === 'script' ||
tag === 'template'
);
case 'html':
return tag === 'head' || tag === 'body' || tag === 'frameset';
case 'frameset':
return tag === 'frame';
case '#document':
return tag === 'html';
}
switch (tag) {
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
return (
parentTag !== 'h1' &&
parentTag !== 'h2' &&
parentTag !== 'h3' &&
parentTag !== 'h4' &&
parentTag !== 'h5' &&
parentTag !== 'h6'
);
case 'rp':
case 'rt':
return impliedEndTags.indexOf(parentTag) === -1;
case 'body':
case 'caption':
case 'col':
case 'colgroup':
case 'frameset':
case 'frame':
case 'head':
case 'html':
case 'tbody':
case 'td':
case 'tfoot':
case 'th':
case 'thead':
case 'tr':
return parentTag == null;
}
return true;
}
function findInvalidAncestorForTag(
tag: string,
ancestorInfo: AncestorInfoDev,
): ?Info {
switch (tag) {
case 'address':
case 'article':
case 'aside':
case 'blockquote':
case 'center':
case 'details':
case 'dialog':
case 'dir':
case 'div':
case 'dl':
case 'fieldset':
case 'figcaption':
case 'figure':
case 'footer':
case 'header':
case 'hgroup':
case 'main':
case 'menu':
case 'nav':
case 'ol':
case 'p':
case 'section':
case 'summary':
case 'ul':
case 'pre':
case 'listing':
case 'table':
case 'hr':
case 'xmp':
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
return ancestorInfo.pTagInButtonScope;
case 'form':
return ancestorInfo.formTag || ancestorInfo.pTagInButtonScope;
case 'li':
return ancestorInfo.listItemTagAutoclosing;
case 'dd':
case 'dt':
return ancestorInfo.dlItemTagAutoclosing;
case 'button':
return ancestorInfo.buttonTagInScope;
case 'a':
return ancestorInfo.aTagInScope;
case 'nobr':
return ancestorInfo.nobrTagInScope;
}
return null;
}
const didWarn: {[string]: boolean} = {};
function validateDOMNesting(
childTag: string,
ancestorInfo: AncestorInfoDev,
): boolean {
if (__DEV__) {
ancestorInfo = ancestorInfo || emptyAncestorInfoDev;
const parentInfo = ancestorInfo.current;
const parentTag = parentInfo && parentInfo.tag;
const invalidParent = isTagValidWithParent(childTag, parentTag)
? null
: parentInfo;
const invalidAncestor = invalidParent
? null
: findInvalidAncestorForTag(childTag, ancestorInfo);
const invalidParentOrAncestor = invalidParent || invalidAncestor;
if (!invalidParentOrAncestor) {
return true;
}
const ancestorTag = invalidParentOrAncestor.tag;
const warnKey =
String(!!invalidParent) + '|' + childTag + '|' + ancestorTag;
if (didWarn[warnKey]) {
return false;
}
didWarn[warnKey] = true;
const tagDisplayName = '<' + childTag + '>';
if (invalidParent) {
let info = '';
if (ancestorTag === 'table' && childTag === 'tr') {
info +=
' Add a <tbody>, <thead> or <tfoot> to your code to match the DOM tree generated by ' +
'the browser.';
}
console['error'](
'In HTML, %s cannot be a child of <%s>.%s\n' +
'This will cause a hydration error.%s',
tagDisplayName,
ancestorTag,
info,
getCurrentParentStackInDev(),
);
} else {
console['error'](
'In HTML, %s cannot be a descendant of <%s>.\n' +
'This will cause a hydration error.%s',
tagDisplayName,
ancestorTag,
getCurrentParentStackInDev(),
);
}
return false;
}
return true;
}
function validateTextNesting(childText: string, parentTag: string): boolean {
if (__DEV__) {
if (isTagValidWithParent('#text', parentTag)) {
return true;
}
const warnKey = '#text|' + parentTag;
if (didWarn[warnKey]) {
return false;
}
didWarn[warnKey] = true;
if (/\S/.test(childText)) {
console['error'](
'In HTML, text nodes cannot be a child of <%s>.\n' +
'This will cause a hydration error.%s',
parentTag,
getCurrentParentStackInDev(),
);
} else {
console['error'](
'In HTML, whitespace text nodes cannot be a child of <%s>. ' +
"Make sure you don't have any extra whitespace between tags on " +
'each line of your source code.\n' +
'This will cause a hydration error.%s',
parentTag,
getCurrentParentStackInDev(),
);
}
return false;
}
return true;
}
export {updatedAncestorInfoDev, validateDOMNesting, validateTextNesting};