UNPKG

13.5 kBJavaScriptView Raw
1/**
2 * Copyright (c) 2015-present, Facebook, Inc.
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 */
8
9'use strict';
10
11var _assign = require('object-assign');
12
13var emptyFunction = require('fbjs/lib/emptyFunction');
14var warning = require('fbjs/lib/warning');
15
16var validateDOMNesting = emptyFunction;
17
18if (process.env.NODE_ENV !== 'production') {
19 // This validation code was written based on the HTML5 parsing spec:
20 // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
21 //
22 // Note: this does not catch all invalid nesting, nor does it try to (as it's
23 // not clear what practical benefit doing so provides); instead, we warn only
24 // for cases where the parser will give a parse tree differing from what React
25 // intended. For example, <b><div></div></b> is invalid but we don't warn
26 // because it still parses correctly; we do warn for other cases like nested
27 // <p> tags where the beginning of the second element implicitly closes the
28 // first, causing a confusing mess.
29
30 // https://html.spec.whatwg.org/multipage/syntax.html#special
31 var 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'];
32
33 // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
34 var inScopeTags = ['applet', 'caption', 'html', 'table', 'td', 'th', 'marquee', 'object', 'template',
35
36 // https://html.spec.whatwg.org/multipage/syntax.html#html-integration-point
37 // TODO: Distinguish by namespace here -- for <title>, including it here
38 // errs on the side of fewer warnings
39 'foreignObject', 'desc', 'title'];
40
41 // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-button-scope
42 var buttonScopeTags = inScopeTags.concat(['button']);
43
44 // https://html.spec.whatwg.org/multipage/syntax.html#generate-implied-end-tags
45 var impliedEndTags = ['dd', 'dt', 'li', 'option', 'optgroup', 'p', 'rp', 'rt'];
46
47 var emptyAncestorInfo = {
48 current: null,
49
50 formTag: null,
51 aTagInScope: null,
52 buttonTagInScope: null,
53 nobrTagInScope: null,
54 pTagInButtonScope: null,
55
56 listItemTagAutoclosing: null,
57 dlItemTagAutoclosing: null
58 };
59
60 var updatedAncestorInfo = function (oldInfo, tag, instance) {
61 var ancestorInfo = _assign({}, oldInfo || emptyAncestorInfo);
62 var info = { tag: tag, instance: instance };
63
64 if (inScopeTags.indexOf(tag) !== -1) {
65 ancestorInfo.aTagInScope = null;
66 ancestorInfo.buttonTagInScope = null;
67 ancestorInfo.nobrTagInScope = null;
68 }
69 if (buttonScopeTags.indexOf(tag) !== -1) {
70 ancestorInfo.pTagInButtonScope = null;
71 }
72
73 // See rules for 'li', 'dd', 'dt' start tags in
74 // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
75 if (specialTags.indexOf(tag) !== -1 && tag !== 'address' && tag !== 'div' && tag !== 'p') {
76 ancestorInfo.listItemTagAutoclosing = null;
77 ancestorInfo.dlItemTagAutoclosing = null;
78 }
79
80 ancestorInfo.current = info;
81
82 if (tag === 'form') {
83 ancestorInfo.formTag = info;
84 }
85 if (tag === 'a') {
86 ancestorInfo.aTagInScope = info;
87 }
88 if (tag === 'button') {
89 ancestorInfo.buttonTagInScope = info;
90 }
91 if (tag === 'nobr') {
92 ancestorInfo.nobrTagInScope = info;
93 }
94 if (tag === 'p') {
95 ancestorInfo.pTagInButtonScope = info;
96 }
97 if (tag === 'li') {
98 ancestorInfo.listItemTagAutoclosing = info;
99 }
100 if (tag === 'dd' || tag === 'dt') {
101 ancestorInfo.dlItemTagAutoclosing = info;
102 }
103
104 return ancestorInfo;
105 };
106
107 /**
108 * Returns whether
109 */
110 var isTagValidWithParent = function (tag, parentTag) {
111 // First, let's check if we're in an unusual parsing mode...
112 switch (parentTag) {
113 // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect
114 case 'select':
115 return tag === 'option' || tag === 'optgroup' || tag === '#text';
116 case 'optgroup':
117 return tag === 'option' || tag === '#text';
118 // Strictly speaking, seeing an <option> doesn't mean we're in a <select>
119 // but
120 case 'option':
121 return tag === '#text';
122 // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intd
123 // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incaption
124 // No special behavior since these rules fall back to "in body" mode for
125 // all except special table nodes which cause bad parsing behavior anyway.
126
127 // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intr
128 case 'tr':
129 return tag === 'th' || tag === 'td' || tag === 'style' || tag === 'script' || tag === 'template';
130 // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intbody
131 case 'tbody':
132 case 'thead':
133 case 'tfoot':
134 return tag === 'tr' || tag === 'style' || tag === 'script' || tag === 'template';
135 // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incolgroup
136 case 'colgroup':
137 return tag === 'col' || tag === 'template';
138 // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intable
139 case 'table':
140 return tag === 'caption' || tag === 'colgroup' || tag === 'tbody' || tag === 'tfoot' || tag === 'thead' || tag === 'style' || tag === 'script' || tag === 'template';
141 // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inhead
142 case 'head':
143 return tag === 'base' || tag === 'basefont' || tag === 'bgsound' || tag === 'link' || tag === 'meta' || tag === 'title' || tag === 'noscript' || tag === 'noframes' || tag === 'style' || tag === 'script' || tag === 'template';
144 // https://html.spec.whatwg.org/multipage/semantics.html#the-html-element
145 case 'html':
146 return tag === 'head' || tag === 'body';
147 case '#document':
148 return tag === 'html';
149 }
150
151 // Probably in the "in body" parsing mode, so we outlaw only tag combos
152 // where the parsing rules cause implicit opens or closes to be added.
153 // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
154 switch (tag) {
155 case 'h1':
156 case 'h2':
157 case 'h3':
158 case 'h4':
159 case 'h5':
160 case 'h6':
161 return parentTag !== 'h1' && parentTag !== 'h2' && parentTag !== 'h3' && parentTag !== 'h4' && parentTag !== 'h5' && parentTag !== 'h6';
162
163 case 'rp':
164 case 'rt':
165 return impliedEndTags.indexOf(parentTag) === -1;
166
167 case 'body':
168 case 'caption':
169 case 'col':
170 case 'colgroup':
171 case 'frame':
172 case 'head':
173 case 'html':
174 case 'tbody':
175 case 'td':
176 case 'tfoot':
177 case 'th':
178 case 'thead':
179 case 'tr':
180 // These tags are only valid with a few parents that have special child
181 // parsing rules -- if we're down here, then none of those matched and
182 // so we allow it only if we don't know what the parent is, as all other
183 // cases are invalid.
184 return parentTag == null;
185 }
186
187 return true;
188 };
189
190 /**
191 * Returns whether
192 */
193 var findInvalidAncestorForTag = function (tag, ancestorInfo) {
194 switch (tag) {
195 case 'address':
196 case 'article':
197 case 'aside':
198 case 'blockquote':
199 case 'center':
200 case 'details':
201 case 'dialog':
202 case 'dir':
203 case 'div':
204 case 'dl':
205 case 'fieldset':
206 case 'figcaption':
207 case 'figure':
208 case 'footer':
209 case 'header':
210 case 'hgroup':
211 case 'main':
212 case 'menu':
213 case 'nav':
214 case 'ol':
215 case 'p':
216 case 'section':
217 case 'summary':
218 case 'ul':
219 case 'pre':
220 case 'listing':
221 case 'table':
222 case 'hr':
223 case 'xmp':
224 case 'h1':
225 case 'h2':
226 case 'h3':
227 case 'h4':
228 case 'h5':
229 case 'h6':
230 return ancestorInfo.pTagInButtonScope;
231
232 case 'form':
233 return ancestorInfo.formTag || ancestorInfo.pTagInButtonScope;
234
235 case 'li':
236 return ancestorInfo.listItemTagAutoclosing;
237
238 case 'dd':
239 case 'dt':
240 return ancestorInfo.dlItemTagAutoclosing;
241
242 case 'button':
243 return ancestorInfo.buttonTagInScope;
244
245 case 'a':
246 // Spec says something about storing a list of markers, but it sounds
247 // equivalent to this check.
248 return ancestorInfo.aTagInScope;
249
250 case 'nobr':
251 return ancestorInfo.nobrTagInScope;
252 }
253
254 return null;
255 };
256
257 /**
258 * Given a ReactCompositeComponent instance, return a list of its recursive
259 * owners, starting at the root and ending with the instance itself.
260 */
261 var findOwnerStack = function (instance) {
262 if (!instance) {
263 return [];
264 }
265
266 var stack = [];
267 do {
268 stack.push(instance);
269 } while (instance = instance._currentElement._owner);
270 stack.reverse();
271 return stack;
272 };
273
274 var didWarn = {};
275
276 validateDOMNesting = function (childTag, childText, childInstance, ancestorInfo) {
277 ancestorInfo = ancestorInfo || emptyAncestorInfo;
278 var parentInfo = ancestorInfo.current;
279 var parentTag = parentInfo && parentInfo.tag;
280
281 if (childText != null) {
282 process.env.NODE_ENV !== 'production' ? warning(childTag == null, 'validateDOMNesting: when childText is passed, childTag should be null') : void 0;
283 childTag = '#text';
284 }
285
286 var invalidParent = isTagValidWithParent(childTag, parentTag) ? null : parentInfo;
287 var invalidAncestor = invalidParent ? null : findInvalidAncestorForTag(childTag, ancestorInfo);
288 var problematic = invalidParent || invalidAncestor;
289
290 if (problematic) {
291 var ancestorTag = problematic.tag;
292 var ancestorInstance = problematic.instance;
293
294 var childOwner = childInstance && childInstance._currentElement._owner;
295 var ancestorOwner = ancestorInstance && ancestorInstance._currentElement._owner;
296
297 var childOwners = findOwnerStack(childOwner);
298 var ancestorOwners = findOwnerStack(ancestorOwner);
299
300 var minStackLen = Math.min(childOwners.length, ancestorOwners.length);
301 var i;
302
303 var deepestCommon = -1;
304 for (i = 0; i < minStackLen; i++) {
305 if (childOwners[i] === ancestorOwners[i]) {
306 deepestCommon = i;
307 } else {
308 break;
309 }
310 }
311
312 var UNKNOWN = '(unknown)';
313 var childOwnerNames = childOwners.slice(deepestCommon + 1).map(function (inst) {
314 return inst.getName() || UNKNOWN;
315 });
316 var ancestorOwnerNames = ancestorOwners.slice(deepestCommon + 1).map(function (inst) {
317 return inst.getName() || UNKNOWN;
318 });
319 var ownerInfo = [].concat(
320 // If the parent and child instances have a common owner ancestor, start
321 // with that -- otherwise we just start with the parent's owners.
322 deepestCommon !== -1 ? childOwners[deepestCommon].getName() || UNKNOWN : [], ancestorOwnerNames, ancestorTag,
323 // If we're warning about an invalid (non-parent) ancestry, add '...'
324 invalidAncestor ? ['...'] : [], childOwnerNames, childTag).join(' > ');
325
326 var warnKey = !!invalidParent + '|' + childTag + '|' + ancestorTag + '|' + ownerInfo;
327 if (didWarn[warnKey]) {
328 return;
329 }
330 didWarn[warnKey] = true;
331
332 var tagDisplayName = childTag;
333 var whitespaceInfo = '';
334 if (childTag === '#text') {
335 if (/\S/.test(childText)) {
336 tagDisplayName = 'Text nodes';
337 } else {
338 tagDisplayName = 'Whitespace text nodes';
339 whitespaceInfo = " Make sure you don't have any extra whitespace between tags on " + 'each line of your source code.';
340 }
341 } else {
342 tagDisplayName = '<' + childTag + '>';
343 }
344
345 if (invalidParent) {
346 var info = '';
347 if (ancestorTag === 'table' && childTag === 'tr') {
348 info += ' Add a <tbody> to your code to match the DOM tree generated by ' + 'the browser.';
349 }
350 process.env.NODE_ENV !== 'production' ? warning(false, 'validateDOMNesting(...): %s cannot appear as a child of <%s>.%s ' + 'See %s.%s', tagDisplayName, ancestorTag, whitespaceInfo, ownerInfo, info) : void 0;
351 } else {
352 process.env.NODE_ENV !== 'production' ? warning(false, 'validateDOMNesting(...): %s cannot appear as a descendant of ' + '<%s>. See %s.', tagDisplayName, ancestorTag, ownerInfo) : void 0;
353 }
354 }
355 };
356
357 validateDOMNesting.updatedAncestorInfo = updatedAncestorInfo;
358
359 // For testing
360 validateDOMNesting.isTagValidInContext = function (tag, ancestorInfo) {
361 ancestorInfo = ancestorInfo || emptyAncestorInfo;
362 var parentInfo = ancestorInfo.current;
363 var parentTag = parentInfo && parentInfo.tag;
364 return isTagValidWithParent(tag, parentTag) && !findInvalidAncestorForTag(tag, ancestorInfo);
365 };
366}
367
368module.exports = validateDOMNesting;
\No newline at end of file