Validate only against problematic tag nesting
Turns out lots of people write invalid HTML all the time and no one, including your browser or React, cares. Most invalid HTML combinations don't cause magic reparented nodes; only some do. The HTML5 parsing spec (https://html.spec.whatwg.org/multipage/syntax.html) specifies which tag combinations cause strange parsing behavior. I did my best to encode the logic here. It's more lenient than before in some cases, but more strict in others (before we didn't look at the whole stack of tags; now we warn with deeply nested `p` or `form` or `a` tags).
Ben Alpert committed
Mar 25, 2015 at 16:43 UTC
76bb96ef21b7c665e1b51b6bb90e64ec40c0f16a
5 files changed
+340
-143
src/browser/__tests__/validateDOMNesting-test.js
new
+79
@@ -0,0 +1,79 @@
1
+/**
2
+ * Copyright 2015, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree. An additional grant
7
+ * of patent rights can be found in the PATENTS file in the same directory.
8
+ *
9
+ * @emails react-core
10
+ */
11
+
12
+'use strict';
13
+
14
+var isTagValidInContext;
15
+
16
+// https://html.spec.whatwg.org/multipage/syntax.html#special
17
+var specialTags = [
18
+ 'address', 'applet', 'area', 'article', 'aside', 'base', 'basefont',
19
+ 'bgsound', 'blockquote', 'body', 'br', 'button', 'caption', 'center', 'col',
20
+ 'colgroup', 'dd', 'details', 'dir', 'div', 'dl', 'dt', 'embed', 'fieldset',
21
+ 'figcaption', 'figure', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2',
22
+ 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'iframe',
23
+ 'img', 'input', 'isindex', 'li', 'link', 'listing', 'main', 'marquee', 'menu',
24
+ 'menuitem', 'meta', 'nav', 'noembed', 'noframes', 'noscript', 'object', 'ol',
25
+ 'p', 'param', 'plaintext', 'pre', 'script', 'section', 'select', 'source',
26
+ 'style', 'summary', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot',
27
+ 'th', 'thead', 'title', 'tr', 'track', 'ul', 'wbr', 'xmp'
28
+];
29
+
30
+// https://html.spec.whatwg.org/multipage/syntax.html#formatting
31
+var formattingTags = [
32
+ 'a', 'b', 'big', 'code', 'em', 'font', 'i', 'nobr', 's', 'small', 'strike',
33
+ 'strong', 'tt', 'u'
34
+];
35
+
36
+function isTagStackValid(stack) {
37
+ for (var i = 0; i < stack.length; i++) {
38
+ if (!isTagValidInContext(stack[i], stack.slice(0, i))) {
39
+ //console.log('invalid', stack[i], stack.slice(0, i));
40
+ return false;
41
+ }
42
+ }
43
+ return true;
44
+}
45
+
46
+describe('ReactContextValidator', function() {
47
+ beforeEach(function() {
48
+ require('mock-modules').dumpCache();
49
+
50
+ isTagValidInContext = require('validateDOMNesting').isTagValidInContext;
51
+ });
52
+
53
+ it('allows any tag with no context', function() {
54
+ // With renderToString (for example), we don't know where we're mounting the
55
+ // tag so we must err on the side of leniency.
56
+ specialTags.concat(formattingTags, ['mysterytag']).forEach(function(tag) {
57
+ expect(isTagValidInContext(tag, [])).toBe(true);
58
+ });
59
+ });
60
+
61
+ it('allows valid nestings', function() {
62
+ expect(isTagStackValid(['table', 'tbody', 'tr', 'td', 'b'])).toBe(true);
63
+ expect(isTagStackValid(['body', 'datalist', 'option'])).toBe(true);
64
+ expect(isTagStackValid(['div', 'a', 'object', 'a'])).toBe(true);
65
+ expect(isTagStackValid(['div', 'p', 'button', 'p'])).toBe(true);
66
+ expect(isTagStackValid(['p', 'svg', 'foreignObject', 'p'])).toBe(true);
67
+
68
+ // Invalid, but not changed by browser parsing so we allow them
69
+ expect(isTagStackValid(['div', 'ul', 'ul', 'li'])).toBe(true);
70
+ expect(isTagStackValid(['div', 'label', 'div'])).toBe(true);
71
+ });
72
+
73
+ it('prevents problematic nestings', function() {
74
+ expect(isTagStackValid(['a', 'a'])).toBe(false);
75
+ expect(isTagStackValid(['form', 'form'])).toBe(false);
76
+ expect(isTagStackValid(['p', 'p'])).toBe(false);
77
+ expect(isTagStackValid(['table', 'tr'])).toBe(false);
78
+ });
79
+});
src/browser/ui/ReactDOMComponent.js
+5
-3
@@ -177,7 +177,8 @@ function processChildContext(context, tagName) {
177
if (__DEV__) {
178
// Pass down our tag name to child components for validation purposes
179
context = assign({}, context);
180
- context[validateDOMNesting.parentTagContextKey] = tagName;
180
+ var stack = context[validateDOMNesting.tagStackContextKey] || [];
181
+ context[validateDOMNesting.tagStackContextKey] = stack.concat([tagName]);
182
}
183
return context;
184
}
@@ -227,9 +228,9 @@ ReactDOMComponent.Mixin = {
228
229
assertValidProps(this, this._currentElement.props);
230
if (__DEV__) {
230
- if (context[validateDOMNesting.parentTagContextKey]) {
231
+ if (context[validateDOMNesting.tagStackContextKey]) {
232
validateDOMNesting(
232
- context[validateDOMNesting.parentTagContextKey],
233
+ context[validateDOMNesting.tagStackContextKey],
234
this._tag,
235
this._currentElement
236
);
@@ -318,6 +319,7 @@ ReactDOMComponent.Mixin = {
319
CONTENT_TYPES[typeof props.children] ? props.children : null;
320
var childrenToUse = contentToUse != null ? null : props.children;
321
if (contentToUse != null) {
322
+ // TODO: Validate that text is allowed as a child of this node
323
ret = escapeTextContentForBrowser(contentToUse);
324
} else if (childrenToUse != null) {
325
var mountImages = this.mountChildren(
src/browser/ui/ReactDOMTextComponent.js
+2
-2
@@ -67,9 +67,9 @@ assign(ReactDOMTextComponent.prototype, {
67
*/
68
mountComponent: function(rootID, transaction, context) {
69
if (__DEV__) {
70
- if (context[validateDOMNesting.parentTagContextKey]) {
70
+ if (context[validateDOMNesting.tagStackContextKey]) {
71
validateDOMNesting(
72
- context[validateDOMNesting.parentTagContextKey],
72
+ context[validateDOMNesting.tagStackContextKey],
73
'span',
74
null
75
);
src/browser/ui/ReactMount.js
+2
-2
@@ -266,8 +266,8 @@ function mountComponentIntoNode(
266
var context = emptyObject;
267
if (__DEV__) {
268
context = {};
269
- context[validateDOMNesting.parentTagContextKey] =
270
- container.nodeName.toLowerCase();
269
+ context[validateDOMNesting.tagStackContextKey] =
270
+ [container.nodeName.toLowerCase()];
271
}
272
var markup = ReactReconciler.mountComponent(
273
componentInstance, rootID, transaction, context
src/browser/validateDOMNesting.js
+252
-136
@@ -17,154 +17,267 @@ var warning = require('warning');
17
var validateDOMNesting = emptyFunction;
18
19
if (__DEV__) {
20
- // The below rules were created from the HTML5 spec and using
21
- // https://github.com/facebook/xhp-lib/blob/1.6.0/src/html.php
22
-
23
- // Flow elements are block or inline elements that can appear in a <div>
24
- var flow = [
25
- 'a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi',
26
- 'bdo', 'blockquote', 'br', 'button', 'canvas', 'cite', 'code', 'data',
27
- 'datalist', 'del', 'details', 'dfn', 'div', 'dl', 'em', 'embed',
28
- 'fieldset', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
29
- 'header', 'hr', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'keygen',
30
- 'label', 'link', 'main', 'map', 'mark', 'menu', 'meta', 'meter', 'nav',
31
- 'noscript', 'object', 'ol', 'output', 'p', 'pre', 'progress', 'q', 'ruby',
32
- 's', 'samp', 'script', 'section', 'select', 'small', 'span', 'strong',
33
- 'style', 'sub', 'sup', 'svg', 'table', 'textarea', 'time', 'u', 'ul',
34
- 'var', 'video', 'wbr', '#text'
35
- ];
20
+ // This validation code was written based on the HTML5 parsing spec:
21
+ // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
22
+ //
23
+ // Note: this does not catch all invalid nesting, nor does it try to (as it's
24
+ // not clear what practical benefit doing so provides); instead, we warn only
25
+ // for cases where the parser will give a parse tree differing from what React
26
+ // intended. For example, <b><div></div></b> is invalid but we don't warn
27
+ // because it still parses correctly; we do warn for other cases like nested
28
+ // <p> tags where the beginning of the second element implicitly closes the
29
+ // first, causing a confusing mess.
30
37
- // Phrasing elements are inline elements that can appear in a <span>
38
- var phrase = [
39
- 'a', 'abbr', 'area', 'audio', 'b', 'bdi', 'bdo', 'br', 'button', 'canvas',
40
- 'cite', 'code', 'data', 'datalist', 'del', 'dfn', 'em', 'embed', 'i',
41
- 'iframe', 'img', 'input', 'ins', 'kbd', 'keygen', 'label', 'link', 'map',
42
- 'mark', 'meta', 'meter', 'noscript', 'object', 'output', 'progress', 'q',
43
- 'ruby', 's', 'samp', 'script', 'select', 'small', 'span', 'strong', 'sub',
44
- 'sup', 'svg', 'textarea', 'time', 'u', 'var', 'video', 'wbr', '#text'
31
+ // https://html.spec.whatwg.org/multipage/syntax.html#special
32
+ var specialTags = [
33
+ 'address', 'applet', 'area', 'article', 'aside', 'base', 'basefont',
34
+ 'bgsound', 'blockquote', 'body', 'br', 'button', 'caption', 'center', 'col',
35
+ 'colgroup', 'dd', 'details', 'dir', 'div', 'dl', 'dt', 'embed', 'fieldset',
36
+ 'figcaption', 'figure', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2',
37
+ 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'iframe',
38
+ 'img', 'input', 'isindex', 'li', 'link', 'listing', 'main', 'marquee',
39
+ 'menu', 'menuitem', 'meta', 'nav', 'noembed', 'noframes', 'noscript',
40
+ 'object', 'ol', 'p', 'param', 'plaintext', 'pre', 'script', 'section',
41
+ 'select', 'source', 'style', 'summary', 'table', 'tbody', 'td', 'template',
42
+ 'textarea', 'tfoot', 'th', 'thead', 'title', 'tr', 'track', 'ul', 'wbr',
43
+ 'xmp'
44
];
45
47
- // Metadata elements can appear in <head>
48
- var metadata = [
49
- 'base', 'link', 'meta', 'noscript', 'script', 'style', 'title'
46
+ /**
47
+ * Return whether `stack` contains `tag` and the last occurrence of `tag` is
48
+ * deeper than any element in the `scope` array.
49
+ *
50
+ * https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-the-specific-scope
51
+ *
52
+ * Examples:
53
+ * stackHasTagInSpecificScope(['p', 'quote'], 'p', ['button']) is true
54
+ * stackHasTagInSpecificScope(['p', 'button'], 'p', ['button']) is false
55
+ *
56
+ * @param {Array<string>} stack
57
+ * @param {string} tag
58
+ * @param {Array<string>} scope
59
+ */
60
+ var stackHasTagInSpecificScope = function(stack, tag, scope) {
61
+ for (var i = stack.length - 1; i >= 0; i--) {
62
+ if (stack[i] === tag) {
63
+ return true;
64
+ }
65
+ if (scope.indexOf(stack[i]) !== -1) {
66
+ return false;
67
+ }
68
+ }
69
+ return false;
70
+ };
71
+
72
+ // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
73
+ var inScopeTags = [
74
+ 'applet', 'caption', 'html', 'table', 'td', 'th', 'marquee', 'object',
75
+ 'template',
76
+
77
+ // https://html.spec.whatwg.org/multipage/syntax.html#html-integration-point
78
+ // TODO: Distinguish by namespace here
79
+ 'foreignObject', 'desc', 'title'
80
];
81
+ var stackHasTagInScope = function(stack, tag) {
82
+ return stackHasTagInSpecificScope(stack, tag, inScopeTags);
83
+ };
84
52
- // By default, we assume that flow elements can contain other flow elements
53
- // and phrasing elements can contain other phrasing elements. Here are the
54
- // exceptions:
55
- var allowedChildren = {
56
- '#document': ['html'],
57
-
58
- 'a': flow,
59
- 'audio': ['source', 'track'].concat(flow),
60
- 'body': flow,
61
- 'button': phrase,
62
- 'caption': flow,
63
- 'canvas': flow,
64
- 'colgroup': ['col'],
65
- 'dd': flow,
66
- 'del': flow,
67
- 'details': ['summary'].concat(flow),
68
- 'dl': ['dt', 'dd'],
69
- 'dt': flow,
70
- 'fieldset': flow,
71
- 'figcaption': flow,
72
- 'figure': ['figcaption'].concat(flow),
73
- 'h1': phrase,
74
- 'h2': phrase,
75
- 'h3': phrase,
76
- 'h4': phrase,
77
- 'h5': phrase,
78
- 'h6': phrase,
79
- 'head': metadata,
80
- 'hgroup': ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
81
- 'html': ['body', 'head'],
82
- 'iframe': [],
83
- 'ins': flow,
84
- 'label': phrase,
85
- 'legend': phrase,
86
- 'li': flow,
87
- 'map': flow,
88
- 'menu': ['li', 'menuitem'].concat(flow),
89
- 'noscript': '*',
90
- 'object': ['param'].concat(flow),
91
- 'ol': ['li'],
92
- 'optgroup': ['option'],
93
- 'p': phrase,
94
- 'pre': phrase,
95
- 'rp': phrase,
96
- 'rt': phrase,
97
- 'ruby': ['rp', 'rt', '#text'],
98
- 'script': ['#text'],
99
- 'select': ['option', 'optgroup'],
100
- 'style': ['#text'],
101
- 'summary': phrase,
102
- 'table': ['caption', 'colgroup', 'tbody', 'tfoot', 'thead'],
103
- 'tbody': ['tr'],
104
- 'td': flow,
105
- 'textarea': ['#text'],
106
- 'tfoot': ['tr'],
107
- 'th': flow,
108
- 'thead': ['tr'],
109
- 'title': ['#text'],
110
- 'tr': ['td', 'th'],
111
- 'ul': ['li'],
112
- 'video': ['source', 'track'].concat(flow),
113
-
114
- // SVG
115
- // TODO: Validate nesting of all svg elements
116
- 'svg': [
117
- 'circle', 'defs', 'g', 'line', 'linearGradient', 'path', 'polygon',
118
- 'polyline', 'radialGradient', 'rect', 'stop', 'text'
119
- ],
120
-
121
- // Self-closing tags
122
- 'area': [],
123
- 'base': [],
124
- 'br': [],
125
- 'col': [],
126
- 'embed': [],
127
- 'hr': [],
128
- 'img': [],
129
- 'input': [],
130
- 'keygen': [],
131
- 'link': [],
132
- 'menuitem': [],
133
- 'meta': [],
134
- 'param': [],
135
- 'source': [],
136
- 'track': [],
137
- 'wbr': []
85
+ // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-button-scope
86
+ var buttonScopeTags = inScopeTags.concat(['button']);
87
+ var stackHasTagInButtonScope = function(stack, tag) {
88
+ return stackHasTagInSpecificScope(stack, tag, buttonScopeTags);
89
};
90
140
- var i, l;
141
- var allowedChildrenMap = {};
142
- for (i = 0, l = flow.length; i < l; i++) {
143
- allowedChildrenMap[flow[i]] = flow;
144
- }
145
- for (i = 0, l = phrase.length; i < l; i++) {
146
- allowedChildrenMap[phrase[i]] = flow;
147
- }
148
- for (var el in allowedChildren) {
149
- if (allowedChildren.hasOwnProperty(el)) {
150
- allowedChildrenMap[el] = allowedChildren[el];
91
+ // See rules for 'li', 'dd', 'dt' start tags in
92
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
93
+ var listItemTagAllowed = function(tags, stack) {
94
+ // tags is ['li'] or ['dd, 'dt']
95
+ for (var i = stack.length - 1; i >= 0; i--) {
96
+ if (tags.indexOf(stack[i]) !== -1) {
97
+ return false;
98
+ } else if (
99
+ specialTags.indexOf(stack[i]) !== -1 &&
100
+ stack[i] !== 'address' && stack[i] !== 'div' && stack[i] !== 'p'
101
+ ) {
102
+ return true;
103
+ }
104
+ }
105
+ return true;
106
+ };
107
+
108
+ // https://html.spec.whatwg.org/multipage/syntax.html#generate-implied-end-tags
109
+ var impliedEndTags =
110
+ ['dd', 'dt', 'li', 'option', 'optgroup', 'p', 'rp', 'rt'];
111
+
112
+ /**
113
+ * Returns whether we allow putting `tag` in the document if the current stack
114
+ * of open tags is `openTagStack`.
115
+ *
116
+ * Examples:
117
+ * isTagValidInContext('tr', [..., 'table', 'tbody']) is true
118
+ * isTagValidInContext('tr', [..., 'table']) is false
119
+ *
120
+ * @param {string} tag Lowercase HTML tag name or node name like '#text'
121
+ * @param {Array<string>} openTagStack
122
+ */
123
+ var isTagValidInContext = function(tag, openTagStack) {
124
+ var currentTag = openTagStack[openTagStack.length - 1];
125
+
126
+ // First, let's check if we're in an unusual parsing mode...
127
+ switch (currentTag) {
128
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect
129
+ case 'select':
130
+ return tag === 'option' || tag === 'optgroup' || tag === '#text';
131
+ case 'optgroup':
132
+ return tag === 'option' || tag === '#text';
133
+ // Strictly speaking, seeing an <option> doesn't mean we're in a <select>
134
+ // but
135
+ case 'option':
136
+ return tag === '#text';
137
+
138
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intd
139
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incaption
140
+ // No special behavior since these rules fall back to "in body" mode for
141
+ // all except special table nodes which cause bad parsing behavior anyway.
142
+
143
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intr
144
+ case 'tr':
145
+ return (
146
+ tag === 'th' || tag === 'td' || tag === 'style' || tag === 'script' ||
147
+ tag === 'template'
148
+ );
149
+
150
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intbody
151
+ case 'tbody':
152
+ case 'thead':
153
+ case 'tfoot':
154
+ return (
155
+ tag === 'tr' || tag === 'style' || tag === 'script' ||
156
+ tag === 'template'
157
+ );
158
+
159
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incolgroup
160
+ case 'colgroup':
161
+ return tag === 'col' || tag === 'template';
162
+
163
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intable
164
+ case 'table':
165
+ return (
166
+ tag === 'caption' || tag === 'colgroup' || tag === 'tbody' ||
167
+ tag === 'tfoot' || tag === 'thead' || tag === 'style' ||
168
+ tag === 'script' || tag === 'template'
169
+ );
170
+
171
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inhead
172
+ case 'head':
173
+ return (
174
+ tag === 'base' || tag === 'basefont' || tag === 'bgsound' ||
175
+ tag === 'link' || tag === 'meta' || tag === 'title' ||
176
+ tag === 'noscript' || tag === 'noframes' || tag === 'style' ||
177
+ tag === 'script' || tag === 'template'
178
+ );
179
}
152
- }
180
154
- var nodeCanContainNode = function(parentTag, childTag) {
155
- var allowed = allowedChildrenMap[parentTag];
156
- if (!allowed || !allowedChildrenMap[childTag]) {
157
- // We don't recognize one of the tags; err on the side of not warning
158
- return true;
181
+ // Probably in the "in body" parsing mode, so we outlaw only tag combos
182
+ // where the parsing rules cause implicit opens or closes to be added.
183
+ // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
184
+ switch (tag) {
185
+ case 'address':
186
+ case 'article':
187
+ case 'aside':
188
+ case 'blockquote':
189
+ case 'center':
190
+ case 'details':
191
+ case 'dialog':
192
+ case 'dir':
193
+ case 'div':
194
+ case 'dl':
195
+ case 'fieldset':
196
+ case 'figcaption':
197
+ case 'figure':
198
+ case 'footer':
199
+ case 'header':
200
+ case 'hgroup':
201
+ case 'main':
202
+ case 'menu':
203
+ case 'nav':
204
+ case 'ol':
205
+ case 'p':
206
+ case 'section':
207
+ case 'summary':
208
+ case 'ul':
209
+
210
+ case 'pre':
211
+ case 'listing':
212
+
213
+ case 'table':
214
+
215
+ case 'hr':
216
+
217
+ case 'xmp':
218
+ return !stackHasTagInButtonScope(openTagStack, 'p');
219
+
220
+ case 'h1':
221
+ case 'h2':
222
+ case 'h3':
223
+ case 'h4':
224
+ case 'h5':
225
+ case 'h6':
226
+ return (
227
+ !stackHasTagInButtonScope(openTagStack, 'p') &&
228
+ currentTag !== 'h1' && currentTag !== 'h2' && currentTag !== 'h3' &&
229
+ currentTag !== 'h4' && currentTag !== 'h5' && currentTag !== 'h6'
230
+ );
231
+
232
+ case 'form':
233
+ return (
234
+ openTagStack.indexOf('form') === -1 &&
235
+ !stackHasTagInButtonScope(openTagStack, 'p')
236
+ );
237
+
238
+ case 'li':
239
+ return listItemTagAllowed(['li'], openTagStack);
240
+
241
+ case 'dd':
242
+ case 'dt':
243
+ return listItemTagAllowed(['dd', 'dt'], openTagStack);
244
+
245
+ case 'button':
246
+ return !stackHasTagInScope(openTagStack, 'button');
247
+
248
+ case 'a':
249
+ // Spec says something about storing a list of markers, but it sounds
250
+ // equivalent to this check.
251
+ return !stackHasTagInScope(openTagStack, 'a');
252
+
253
+ case 'nobr':
254
+ return !stackHasTagInScope(openTagStack, 'nobr');
255
+
256
+ case 'rp':
257
+ case 'rt':
258
+ return impliedEndTags.indexOf(currentTag) === -1;
259
+
260
+ case 'caption':
261
+ case 'col':
262
+ case 'colgroup':
263
+ case 'frame':
264
+ case 'head':
265
+ case 'tbody':
266
+ case 'td':
267
+ case 'tfoot':
268
+ case 'th':
269
+ case 'thead':
270
+ case 'tr':
271
+ return currentTag === undefined;
272
}
273
161
- var result = allowed === '*' || allowed.indexOf(childTag) !== -1;
162
- return !!result;
274
+ return true;
275
};
276
165
- validateDOMNesting = function(parentTag, childTag, element) {
166
- if (!nodeCanContainNode(parentTag, childTag)) {
277
+ validateDOMNesting = function(parentStack, childTag, element) {
278
+ if (!isTagValidInContext(childTag, parentStack)) {
279
var info = '';
280
+ var parentTag = parentStack[parentStack.length - 1];
281
if (parentTag === 'table' && childTag === 'tr') {
282
info +=
283
' Add a <tbody> to your code to match the DOM tree generated by ' +
@@ -187,8 +300,11 @@ if (__DEV__) {
300
}
301
};
302
190
- validateDOMNesting.parentTagContextKey =
191
- '__validateDOMNesting_parentTag$' + Math.random().toString(36).slice(2);
303
+ validateDOMNesting.tagStackContextKey =
304
+ '__validateDOMNesting_tagStack$' + Math.random().toString(36).slice(2);
305
+
306
+ // For testing
307
+ validateDOMNesting.isTagValidInContext = isTagValidInContext;
308
}
309
310
module.exports = validateDOMNesting;