UNPKG

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