1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @flow
    
  8.  */
    
  9. 
    
  10. type Info = {tag: string};
    
  11. export type AncestorInfoDev = {
    
  12.   current: ?Info,
    
  13. 
    
  14.   formTag: ?Info,
    
  15.   aTagInScope: ?Info,
    
  16.   buttonTagInScope: ?Info,
    
  17.   nobrTagInScope: ?Info,
    
  18.   pTagInButtonScope: ?Info,
    
  19. 
    
  20.   listItemTagAutoclosing: ?Info,
    
  21.   dlItemTagAutoclosing: ?Info,
    
  22. 
    
  23.   // <head> or <body>
    
  24.   containerTagInScope: ?Info,
    
  25. };
    
  26. 
    
  27. // This validation code was written based on the HTML5 parsing spec:
    
  28. // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
    
  29. //
    
  30. // Note: this does not catch all invalid nesting, nor does it try to (as it's
    
  31. // not clear what practical benefit doing so provides); instead, we warn only
    
  32. // for cases where the parser will give a parse tree differing from what React
    
  33. // intended. For example, <b><div></div></b> is invalid but we don't warn
    
  34. // because it still parses correctly; we do warn for other cases like nested
    
  35. // <p> tags where the beginning of the second element implicitly closes the
    
  36. // first, causing a confusing mess.
    
  37. 
    
  38. // https://html.spec.whatwg.org/multipage/syntax.html#special
    
  39. const specialTags = [
    
  40.   'address',
    
  41.   'applet',
    
  42.   'area',
    
  43.   'article',
    
  44.   'aside',
    
  45.   'base',
    
  46.   'basefont',
    
  47.   'bgsound',
    
  48.   'blockquote',
    
  49.   'body',
    
  50.   'br',
    
  51.   'button',
    
  52.   'caption',
    
  53.   'center',
    
  54.   'col',
    
  55.   'colgroup',
    
  56.   'dd',
    
  57.   'details',
    
  58.   'dir',
    
  59.   'div',
    
  60.   'dl',
    
  61.   'dt',
    
  62.   'embed',
    
  63.   'fieldset',
    
  64.   'figcaption',
    
  65.   'figure',
    
  66.   'footer',
    
  67.   'form',
    
  68.   'frame',
    
  69.   'frameset',
    
  70.   'h1',
    
  71.   'h2',
    
  72.   'h3',
    
  73.   'h4',
    
  74.   'h5',
    
  75.   'h6',
    
  76.   'head',
    
  77.   'header',
    
  78.   'hgroup',
    
  79.   'hr',
    
  80.   'html',
    
  81.   'iframe',
    
  82.   'img',
    
  83.   'input',
    
  84.   'isindex',
    
  85.   'li',
    
  86.   'link',
    
  87.   'listing',
    
  88.   'main',
    
  89.   'marquee',
    
  90.   'menu',
    
  91.   'menuitem',
    
  92.   'meta',
    
  93.   'nav',
    
  94.   'noembed',
    
  95.   'noframes',
    
  96.   'noscript',
    
  97.   'object',
    
  98.   'ol',
    
  99.   'p',
    
  100.   'param',
    
  101.   'plaintext',
    
  102.   'pre',
    
  103.   'script',
    
  104.   'section',
    
  105.   'select',
    
  106.   'source',
    
  107.   'style',
    
  108.   'summary',
    
  109.   'table',
    
  110.   'tbody',
    
  111.   'td',
    
  112.   'template',
    
  113.   'textarea',
    
  114.   'tfoot',
    
  115.   'th',
    
  116.   'thead',
    
  117.   'title',
    
  118.   'tr',
    
  119.   'track',
    
  120.   'ul',
    
  121.   'wbr',
    
  122.   'xmp',
    
  123. ];
    
  124. 
    
  125. // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
    
  126. const inScopeTags = [
    
  127.   'applet',
    
  128.   'caption',
    
  129.   'html',
    
  130.   'table',
    
  131.   'td',
    
  132.   'th',
    
  133.   'marquee',
    
  134.   'object',
    
  135.   'template',
    
  136. 
    
  137.   // https://html.spec.whatwg.org/multipage/syntax.html#html-integration-point
    
  138.   // TODO: Distinguish by namespace here -- for <title>, including it here
    
  139.   // errs on the side of fewer warnings
    
  140.   'foreignObject',
    
  141.   'desc',
    
  142.   'title',
    
  143. ];
    
  144. 
    
  145. // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-button-scope
    
  146. const buttonScopeTags = __DEV__ ? inScopeTags.concat(['button']) : [];
    
  147. 
    
  148. // https://html.spec.whatwg.org/multipage/syntax.html#generate-implied-end-tags
    
  149. const impliedEndTags = [
    
  150.   'dd',
    
  151.   'dt',
    
  152.   'li',
    
  153.   'option',
    
  154.   'optgroup',
    
  155.   'p',
    
  156.   'rp',
    
  157.   'rt',
    
  158. ];
    
  159. 
    
  160. const emptyAncestorInfoDev: AncestorInfoDev = {
    
  161.   current: null,
    
  162. 
    
  163.   formTag: null,
    
  164.   aTagInScope: null,
    
  165.   buttonTagInScope: null,
    
  166.   nobrTagInScope: null,
    
  167.   pTagInButtonScope: null,
    
  168. 
    
  169.   listItemTagAutoclosing: null,
    
  170.   dlItemTagAutoclosing: null,
    
  171. 
    
  172.   containerTagInScope: null,
    
  173. };
    
  174. 
    
  175. function updatedAncestorInfoDev(
    
  176.   oldInfo: ?AncestorInfoDev,
    
  177.   tag: string,
    
  178. ): AncestorInfoDev {
    
  179.   if (__DEV__) {
    
  180.     const ancestorInfo = {...(oldInfo || emptyAncestorInfoDev)};
    
  181.     const info = {tag};
    
  182. 
    
  183.     if (inScopeTags.indexOf(tag) !== -1) {
    
  184.       ancestorInfo.aTagInScope = null;
    
  185.       ancestorInfo.buttonTagInScope = null;
    
  186.       ancestorInfo.nobrTagInScope = null;
    
  187.     }
    
  188.     if (buttonScopeTags.indexOf(tag) !== -1) {
    
  189.       ancestorInfo.pTagInButtonScope = null;
    
  190.     }
    
  191. 
    
  192.     // See rules for 'li', 'dd', 'dt' start tags in
    
  193.     // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
    
  194.     if (
    
  195.       specialTags.indexOf(tag) !== -1 &&
    
  196.       tag !== 'address' &&
    
  197.       tag !== 'div' &&
    
  198.       tag !== 'p'
    
  199.     ) {
    
  200.       ancestorInfo.listItemTagAutoclosing = null;
    
  201.       ancestorInfo.dlItemTagAutoclosing = null;
    
  202.     }
    
  203. 
    
  204.     ancestorInfo.current = info;
    
  205. 
    
  206.     if (tag === 'form') {
    
  207.       ancestorInfo.formTag = info;
    
  208.     }
    
  209.     if (tag === 'a') {
    
  210.       ancestorInfo.aTagInScope = info;
    
  211.     }
    
  212.     if (tag === 'button') {
    
  213.       ancestorInfo.buttonTagInScope = info;
    
  214.     }
    
  215.     if (tag === 'nobr') {
    
  216.       ancestorInfo.nobrTagInScope = info;
    
  217.     }
    
  218.     if (tag === 'p') {
    
  219.       ancestorInfo.pTagInButtonScope = info;
    
  220.     }
    
  221.     if (tag === 'li') {
    
  222.       ancestorInfo.listItemTagAutoclosing = info;
    
  223.     }
    
  224.     if (tag === 'dd' || tag === 'dt') {
    
  225.       ancestorInfo.dlItemTagAutoclosing = info;
    
  226.     }
    
  227.     if (tag === '#document' || tag === 'html') {
    
  228.       ancestorInfo.containerTagInScope = null;
    
  229.     } else if (!ancestorInfo.containerTagInScope) {
    
  230.       ancestorInfo.containerTagInScope = info;
    
  231.     }
    
  232. 
    
  233.     return ancestorInfo;
    
  234.   } else {
    
  235.     return (null: any);
    
  236.   }
    
  237. }
    
  238. 
    
  239. /**
    
  240.  * Returns whether
    
  241.  */
    
  242. function isTagValidWithParent(tag: string, parentTag: ?string): boolean {
    
  243.   // First, let's check if we're in an unusual parsing mode...
    
  244.   switch (parentTag) {
    
  245.     // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect
    
  246.     case 'select':
    
  247.       return (
    
  248.         tag === 'hr' ||
    
  249.         tag === 'option' ||
    
  250.         tag === 'optgroup' ||
    
  251.         tag === '#text'
    
  252.       );
    
  253.     case 'optgroup':
    
  254.       return tag === 'option' || tag === '#text';
    
  255.     // Strictly speaking, seeing an <option> doesn't mean we're in a <select>
    
  256.     // but
    
  257.     case 'option':
    
  258.       return tag === '#text';
    
  259.     // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intd
    
  260.     // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incaption
    
  261.     // No special behavior since these rules fall back to "in body" mode for
    
  262.     // all except special table nodes which cause bad parsing behavior anyway.
    
  263. 
    
  264.     // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intr
    
  265.     case 'tr':
    
  266.       return (
    
  267.         tag === 'th' ||
    
  268.         tag === 'td' ||
    
  269.         tag === 'style' ||
    
  270.         tag === 'script' ||
    
  271.         tag === 'template'
    
  272.       );
    
  273.     // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intbody
    
  274.     case 'tbody':
    
  275.     case 'thead':
    
  276.     case 'tfoot':
    
  277.       return (
    
  278.         tag === 'tr' ||
    
  279.         tag === 'style' ||
    
  280.         tag === 'script' ||
    
  281.         tag === 'template'
    
  282.       );
    
  283.     // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incolgroup
    
  284.     case 'colgroup':
    
  285.       return tag === 'col' || tag === 'template';
    
  286.     // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intable
    
  287.     case 'table':
    
  288.       return (
    
  289.         tag === 'caption' ||
    
  290.         tag === 'colgroup' ||
    
  291.         tag === 'tbody' ||
    
  292.         tag === 'tfoot' ||
    
  293.         tag === 'thead' ||
    
  294.         tag === 'style' ||
    
  295.         tag === 'script' ||
    
  296.         tag === 'template'
    
  297.       );
    
  298.     // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inhead
    
  299.     case 'head':
    
  300.       return (
    
  301.         tag === 'base' ||
    
  302.         tag === 'basefont' ||
    
  303.         tag === 'bgsound' ||
    
  304.         tag === 'link' ||
    
  305.         tag === 'meta' ||
    
  306.         tag === 'title' ||
    
  307.         tag === 'noscript' ||
    
  308.         tag === 'noframes' ||
    
  309.         tag === 'style' ||
    
  310.         tag === 'script' ||
    
  311.         tag === 'template'
    
  312.       );
    
  313.     // https://html.spec.whatwg.org/multipage/semantics.html#the-html-element
    
  314.     case 'html':
    
  315.       return tag === 'head' || tag === 'body' || tag === 'frameset';
    
  316.     case 'frameset':
    
  317.       return tag === 'frame';
    
  318.     case '#document':
    
  319.       return tag === 'html';
    
  320.   }
    
  321. 
    
  322.   // Probably in the "in body" parsing mode, so we outlaw only tag combos
    
  323.   // where the parsing rules cause implicit opens or closes to be added.
    
  324.   // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
    
  325.   switch (tag) {
    
  326.     case 'h1':
    
  327.     case 'h2':
    
  328.     case 'h3':
    
  329.     case 'h4':
    
  330.     case 'h5':
    
  331.     case 'h6':
    
  332.       return (
    
  333.         parentTag !== 'h1' &&
    
  334.         parentTag !== 'h2' &&
    
  335.         parentTag !== 'h3' &&
    
  336.         parentTag !== 'h4' &&
    
  337.         parentTag !== 'h5' &&
    
  338.         parentTag !== 'h6'
    
  339.       );
    
  340. 
    
  341.     case 'rp':
    
  342.     case 'rt':
    
  343.       return impliedEndTags.indexOf(parentTag) === -1;
    
  344. 
    
  345.     case 'body':
    
  346.     case 'caption':
    
  347.     case 'col':
    
  348.     case 'colgroup':
    
  349.     case 'frameset':
    
  350.     case 'frame':
    
  351.     case 'head':
    
  352.     case 'html':
    
  353.     case 'tbody':
    
  354.     case 'td':
    
  355.     case 'tfoot':
    
  356.     case 'th':
    
  357.     case 'thead':
    
  358.     case 'tr':
    
  359.       // These tags are only valid with a few parents that have special child
    
  360.       // parsing rules -- if we're down here, then none of those matched and
    
  361.       // so we allow it only if we don't know what the parent is, as all other
    
  362.       // cases are invalid.
    
  363.       return parentTag == null;
    
  364.   }
    
  365. 
    
  366.   return true;
    
  367. }
    
  368. 
    
  369. /**
    
  370.  * Returns whether
    
  371.  */
    
  372. function findInvalidAncestorForTag(
    
  373.   tag: string,
    
  374.   ancestorInfo: AncestorInfoDev,
    
  375. ): ?Info {
    
  376.   switch (tag) {
    
  377.     case 'address':
    
  378.     case 'article':
    
  379.     case 'aside':
    
  380.     case 'blockquote':
    
  381.     case 'center':
    
  382.     case 'details':
    
  383.     case 'dialog':
    
  384.     case 'dir':
    
  385.     case 'div':
    
  386.     case 'dl':
    
  387.     case 'fieldset':
    
  388.     case 'figcaption':
    
  389.     case 'figure':
    
  390.     case 'footer':
    
  391.     case 'header':
    
  392.     case 'hgroup':
    
  393.     case 'main':
    
  394.     case 'menu':
    
  395.     case 'nav':
    
  396.     case 'ol':
    
  397.     case 'p':
    
  398.     case 'section':
    
  399.     case 'summary':
    
  400.     case 'ul':
    
  401.     case 'pre':
    
  402.     case 'listing':
    
  403.     case 'table':
    
  404.     case 'hr':
    
  405.     case 'xmp':
    
  406.     case 'h1':
    
  407.     case 'h2':
    
  408.     case 'h3':
    
  409.     case 'h4':
    
  410.     case 'h5':
    
  411.     case 'h6':
    
  412.       return ancestorInfo.pTagInButtonScope;
    
  413. 
    
  414.     case 'form':
    
  415.       return ancestorInfo.formTag || ancestorInfo.pTagInButtonScope;
    
  416. 
    
  417.     case 'li':
    
  418.       return ancestorInfo.listItemTagAutoclosing;
    
  419. 
    
  420.     case 'dd':
    
  421.     case 'dt':
    
  422.       return ancestorInfo.dlItemTagAutoclosing;
    
  423. 
    
  424.     case 'button':
    
  425.       return ancestorInfo.buttonTagInScope;
    
  426. 
    
  427.     case 'a':
    
  428.       // Spec says something about storing a list of markers, but it sounds
    
  429.       // equivalent to this check.
    
  430.       return ancestorInfo.aTagInScope;
    
  431. 
    
  432.     case 'nobr':
    
  433.       return ancestorInfo.nobrTagInScope;
    
  434.   }
    
  435. 
    
  436.   return null;
    
  437. }
    
  438. 
    
  439. const didWarn: {[string]: boolean} = {};
    
  440. 
    
  441. function validateDOMNesting(
    
  442.   childTag: string,
    
  443.   ancestorInfo: AncestorInfoDev,
    
  444. ): void {
    
  445.   if (__DEV__) {
    
  446.     ancestorInfo = ancestorInfo || emptyAncestorInfoDev;
    
  447.     const parentInfo = ancestorInfo.current;
    
  448.     const parentTag = parentInfo && parentInfo.tag;
    
  449. 
    
  450.     const invalidParent = isTagValidWithParent(childTag, parentTag)
    
  451.       ? null
    
  452.       : parentInfo;
    
  453.     const invalidAncestor = invalidParent
    
  454.       ? null
    
  455.       : findInvalidAncestorForTag(childTag, ancestorInfo);
    
  456.     const invalidParentOrAncestor = invalidParent || invalidAncestor;
    
  457.     if (!invalidParentOrAncestor) {
    
  458.       return;
    
  459.     }
    
  460. 
    
  461.     const ancestorTag = invalidParentOrAncestor.tag;
    
  462. 
    
  463.     const warnKey =
    
  464.       // eslint-disable-next-line react-internal/safe-string-coercion
    
  465.       String(!!invalidParent) + '|' + childTag + '|' + ancestorTag;
    
  466.     if (didWarn[warnKey]) {
    
  467.       return;
    
  468.     }
    
  469.     didWarn[warnKey] = true;
    
  470. 
    
  471.     const tagDisplayName = '<' + childTag + '>';
    
  472.     if (invalidParent) {
    
  473.       let info = '';
    
  474.       if (ancestorTag === 'table' && childTag === 'tr') {
    
  475.         info +=
    
  476.           ' Add a <tbody>, <thead> or <tfoot> to your code to match the DOM tree generated by ' +
    
  477.           'the browser.';
    
  478.       }
    
  479.       console.error(
    
  480.         'validateDOMNesting(...): %s cannot appear as a child of <%s>.%s',
    
  481.         tagDisplayName,
    
  482.         ancestorTag,
    
  483.         info,
    
  484.       );
    
  485.     } else {
    
  486.       console.error(
    
  487.         'validateDOMNesting(...): %s cannot appear as a descendant of ' +
    
  488.           '<%s>.',
    
  489.         tagDisplayName,
    
  490.         ancestorTag,
    
  491.       );
    
  492.     }
    
  493.   }
    
  494. }
    
  495. 
    
  496. function validateTextNesting(childText: string, parentTag: string): void {
    
  497.   if (__DEV__) {
    
  498.     if (isTagValidWithParent('#text', parentTag)) {
    
  499.       return;
    
  500.     }
    
  501. 
    
  502.     // eslint-disable-next-line react-internal/safe-string-coercion
    
  503.     const warnKey = '#text|' + parentTag;
    
  504.     if (didWarn[warnKey]) {
    
  505.       return;
    
  506.     }
    
  507.     didWarn[warnKey] = true;
    
  508. 
    
  509.     if (/\S/.test(childText)) {
    
  510.       console.error(
    
  511.         'validateDOMNesting(...): Text nodes cannot appear as a child of <%s>.',
    
  512.         parentTag,
    
  513.       );
    
  514.     } else {
    
  515.       console.error(
    
  516.         'validateDOMNesting(...): Whitespace text nodes cannot appear as a child of <%s>. ' +
    
  517.           "Make sure you don't have any extra whitespace between tags on " +
    
  518.           'each line of your source code.',
    
  519.         parentTag,
    
  520.       );
    
  521.     }
    
  522.   }
    
  523. }
    
  524. 
    
  525. export {updatedAncestorInfoDev, validateDOMNesting, validateTextNesting};