1
+/**
2
+ * Copyright 2013-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
+ * @providesModule ReactServerRenderer
10
+ */
11
+
12
+'use strict';
13
+
14
+var CSSPropertyOperations = require('CSSPropertyOperations');
15
+var DOMPropertyOperations = require('DOMPropertyOperations');
16
+var {registrationNameModules} = require('EventPluginRegistry');
17
+var React = require('React');
18
+var ReactControlledValuePropTypes = require('ReactControlledValuePropTypes');
19
+var ReactElement = require('ReactElement');
20
+var ReactMarkupChecksum = require('ReactMarkupChecksum');
21
+
22
+var assertValidProps = require('assertValidProps');
23
+var checkPropTypes = require('prop-types/checkPropTypes');
24
+var emptyObject = require('fbjs/lib/emptyObject');
25
+var escapeTextContentForBrowser = require('escapeTextContentForBrowser');
26
+var invariant = require('fbjs/lib/invariant');
27
+var omittedCloseTags = require('omittedCloseTags');
28
+var traverseAllChildren = require('traverseAllChildren');
29
+var warning = require('fbjs/lib/warning');
30
+
31
+if (__DEV__) {
32
+ var {
33
+ validateProperties: validateARIAProperties,
34
+ } = require('ReactDOMInvalidARIAHook');
35
+ var {
36
+ validateProperties: validateInputPropertes,
37
+ } = require('ReactDOMNullInputValuePropHook');
38
+ var {
39
+ validateProperties: validateUnknownPropertes,
40
+ } = require('ReactDOMUnknownPropertyHook');
41
+ var validatePropertiesInDevelopment = function(type, props) {
42
+ validateARIAProperties(type, props);
43
+ validateInputPropertes(type, props);
44
+ validateUnknownPropertes(type, props);
45
+ };
46
+}
47
+
48
+var didWarnDefaultInputValue = false;
49
+var didWarnDefaultChecked = false;
50
+var didWarnDefaultSelectValue = false;
51
+var didWarnDefaultTextareaValue = false;
52
+var didWarnInvalidOptionChildren = false;
53
+var valuePropNames = ['value', 'defaultValue'];
54
+var newlineEatingTags = {
55
+ listing: true,
56
+ pre: true,
57
+ textarea: true,
58
+};
59
+
60
+// We accept any tag to be rendered but since this gets injected into arbitrary
61
+// HTML, we want to make sure that it's a safe tag.
62
+// http://www.w3.org/TR/REC-xml/#NT-Name
63
+var VALID_TAG_REGEX = /^[a-zA-Z][a-zA-Z:_\.\-\d]*$/; // Simplified subset
64
+var validatedTagCache = {};
65
+function validateDangerousTag(tag) {
66
+ if (!validatedTagCache.hasOwnProperty(tag)) {
67
+ invariant(VALID_TAG_REGEX.test(tag), 'Invalid tag: %s', tag);
68
+ validatedTagCache[tag] = true;
69
+ }
70
+}
71
+
72
+function warnNoop(
73
+ publicInstance: ReactComponent<any, any, any>,
74
+ callerName: string,
75
+) {
76
+ if (__DEV__) {
77
+ var constructor = publicInstance.constructor;
78
+ warning(
79
+ false,
80
+ '%s(...): Can only update a mounting component. ' +
81
+ 'This usually means you called %s() outside componentWillMount() on the server. ' +
82
+ 'This is a no-op.\n\nPlease check the code for the %s component.',
83
+ callerName,
84
+ callerName,
85
+ (constructor && (constructor.displayName || constructor.name)) ||
86
+ 'ReactClass',
87
+ );
88
+ }
89
+}
90
+
91
+function shouldConstruct(Component) {
92
+ return Component.prototype && Component.prototype.isReactComponent;
93
+}
94
+
95
+function getNonChildrenInnerMarkup(props) {
96
+ var innerHTML = props.dangerouslySetInnerHTML;
97
+ if (innerHTML != null) {
98
+ if (innerHTML.__html != null) {
99
+ return innerHTML.__html;
100
+ }
101
+ } else {
102
+ var content = props.children;
103
+ if (typeof content === 'string' || typeof content === 'number') {
104
+ return escapeTextContentForBrowser(content);
105
+ }
106
+ }
107
+ return null;
108
+}
109
+
110
+function flattenOptionChildren(children) {
111
+ var content = '';
112
+ // Flatten children and warn if they aren't strings or numbers;
113
+ // invalid types are ignored.
114
+ React.Children.forEach(children, function(child) {
115
+ if (child == null) {
116
+ return;
117
+ }
118
+ if (typeof child === 'string' || typeof child === 'number') {
119
+ content += child;
120
+ } else if (!didWarnInvalidOptionChildren) {
121
+ didWarnInvalidOptionChildren = true;
122
+ warning(
123
+ false,
124
+ 'Only strings and numbers are supported as <option> children.',
125
+ );
126
+ }
127
+ });
128
+ return content;
129
+}
130
+
131
+function maskContext(type, context) {
132
+ var contextTypes = type.contextTypes;
133
+ if (!contextTypes) {
134
+ return emptyObject;
135
+ }
136
+ var maskedContext = {};
137
+ for (var contextName in contextTypes) {
138
+ maskedContext[contextName] = context[contextName];
139
+ }
140
+ return maskedContext;
141
+}
142
+
143
+function checkContextTypes(typeSpecs, values, location: string) {
144
+ if (__DEV__) {
145
+ checkPropTypes(
146
+ typeSpecs,
147
+ values,
148
+ location,
149
+ 'Component',
150
+ () => '', // ReactDebugCurrentFrame.getStackAddendum,
151
+ );
152
+ }
153
+}
154
+
155
+function processContext(type, context) {
156
+ var maskedContext = maskContext(type, context);
157
+ if (__DEV__) {
158
+ if (type.contextTypes) {
159
+ checkContextTypes(type.contextTypes, maskedContext, 'context');
160
+ }
161
+ }
162
+ return maskedContext;
163
+}
164
+
165
+var STYLE = 'style';
166
+var RESERVED_PROPS = {
167
+ children: null,
168
+ dangerouslySetInnerHTML: null,
169
+ suppressContentEditableWarning: null,
170
+};
171
+
172
+function isCustomComponent(tagName, props) {
173
+ return tagName.indexOf('-') >= 0 || props.is != null;
174
+}
175
+
176
+function createOpenTagMarkup(
177
+ tagVerbatim,
178
+ tagLowercase,
179
+ props,
180
+ makeStaticMarkup,
181
+ isRootElement,
182
+ domID,
183
+ instForDebug,
184
+) {
185
+ var ret = '<' + tagVerbatim;
186
+
187
+ for (var propKey in props) {
188
+ if (!props.hasOwnProperty(propKey)) {
189
+ continue;
190
+ }
191
+ var propValue = props[propKey];
192
+ if (propValue == null) {
193
+ continue;
194
+ }
195
+ if (!registrationNameModules.hasOwnProperty(propKey)) {
196
+ if (propKey === STYLE) {
197
+ propValue = CSSPropertyOperations.createMarkupForStyles(
198
+ propValue,
199
+ instForDebug,
200
+ );
201
+ }
202
+ var markup = null;
203
+ if (isCustomComponent(tagLowercase, props)) {
204
+ if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
205
+ markup = DOMPropertyOperations.createMarkupForCustomAttribute(
206
+ propKey,
207
+ propValue,
208
+ );
209
+ }
210
+ } else {
211
+ markup = DOMPropertyOperations.createMarkupForProperty(
212
+ propKey,
213
+ propValue,
214
+ );
215
+ }
216
+ if (markup) {
217
+ ret += ' ' + markup;
218
+ }
219
+ }
220
+ }
221
+
222
+ // For static pages, no need to put React ID and checksum. Saves lots of
223
+ // bytes.
224
+ if (makeStaticMarkup) {
225
+ return ret;
226
+ }
227
+
228
+ if (isRootElement) {
229
+ ret += ' ' + DOMPropertyOperations.createMarkupForRoot();
230
+ }
231
+ ret += ' ' + DOMPropertyOperations.createMarkupForID(domID);
232
+ return ret;
233
+}
234
+
235
+function resolve(child, context) {
236
+ // TODO: We'll need to support Arrays (and strings) after Fiber is rolled out
237
+ invariant(!Array.isArray(child), 'Did not expect to receive an Array child');
238
+ while (
239
+ ReactElement.isValidElement(child) &&
240
+ typeof child.type === 'function'
241
+ ) {
242
+ var Component = child.type;
243
+ var publicContext = processContext(Component, context);
244
+
245
+ var inst;
246
+ var queue = [];
247
+ var replace = false;
248
+ var updater = {
249
+ isMounted: function(publicInstance) {
250
+ return false;
251
+ },
252
+ enqueueForceUpdate: function(publicInstance) {
253
+ if (queue === null) {
254
+ warnNoop(publicInstance, 'forceUpdate');
255
+ return null;
256
+ }
257
+ },
258
+ enqueueReplaceState: function(publicInstance, completeState) {
259
+ replace = true;
260
+ queue = [completeState];
261
+ },
262
+ enqueueSetState: function(publicInstance, partialState) {
263
+ if (queue === null) {
264
+ warnNoop(publicInstance, 'setState');
265
+ return null;
266
+ }
267
+ queue.push(partialState);
268
+ },
269
+ };
270
+
271
+ if (shouldConstruct(Component)) {
272
+ inst = new Component(child.props, publicContext, updater);
273
+ } else {
274
+ inst = Component(child.props, publicContext, updater);
275
+ if (inst == null || inst.render == null) {
276
+ child = inst;
277
+ continue;
278
+ }
279
+ }
280
+
281
+ var initialState = inst.state;
282
+ if (initialState === undefined) {
283
+ inst.state = initialState = null;
284
+ }
285
+ if (inst.componentWillMount) {
286
+ inst.componentWillMount();
287
+ if (queue.length) {
288
+ var oldQueue = queue;
289
+ var oldReplace = replace;
290
+ queue = null;
291
+ replace = false;
292
+
293
+ if (oldReplace && oldQueue.length === 1) {
294
+ inst.state = oldQueue[0];
295
+ } else {
296
+ var nextState = oldReplace ? oldQueue[0] : inst.state;
297
+ var dontMutate = true;
298
+ for (var i = oldReplace ? 1 : 0; i < oldQueue.length; i++) {
299
+ var partial = oldQueue[i];
300
+ var partialState = typeof partial === 'function'
301
+ ? partial.call(inst, nextState, child.props, publicContext)
302
+ : partial;
303
+ if (partialState) {
304
+ if (dontMutate) {
305
+ dontMutate = false;
306
+ nextState = Object.assign({}, nextState, partialState);
307
+ } else {
308
+ Object.assign(nextState, partialState);
309
+ }
310
+ }
311
+ }
312
+ inst.state = nextState;
313
+ }
314
+ } else {
315
+ queue = null;
316
+ }
317
+ }
318
+ child = inst.render();
319
+
320
+ var childContext = inst.getChildContext && inst.getChildContext();
321
+ if (childContext) {
322
+ context = Object.assign({}, context, childContext);
323
+ }
324
+ }
325
+ return {child, context};
326
+}
327
+
328
+class ReactDOMServerRenderer {
329
+ constructor(element, makeStaticMarkup) {
330
+ this.stack = [
331
+ {
332
+ children: [element],
333
+ childIndex: 0,
334
+ context: emptyObject,
335
+ footer: '',
336
+ },
337
+ ];
338
+ this.idCounter = 1;
339
+ this.exhausted = false;
340
+ this.currentSelectValue = null;
341
+ this.makeStaticMarkup = makeStaticMarkup;
342
+ }
343
+
344
+ read(bytes) {
345
+ var out = '';
346
+ while (out.length < bytes) {
347
+ if (this.stack.length === 0) {
348
+ this.exhausted = true;
349
+ break;
350
+ }
351
+ var frame = this.stack[this.stack.length - 1];
352
+ if (frame.childIndex >= frame.children.length) {
353
+ out += frame.footer;
354
+ this.stack.pop();
355
+ if (frame.tag === 'select') {
356
+ this.currentSelectValue = null;
357
+ }
358
+ continue;
359
+ }
360
+ var child = frame.children[frame.childIndex++];
361
+ out += this.render(child, frame.context);
362
+ }
363
+ return out;
364
+ }
365
+
366
+ render(child, context) {
367
+ if (typeof child === 'string' || typeof child === 'number') {
368
+ if (this.makeStaticMarkup) {
369
+ return escapeTextContentForBrowser('' + child);
370
+ }
371
+ return (
372
+ '<!-- react-text: ' +
373
+ this.idCounter++ +
374
+ ' -->' +
375
+ escapeTextContentForBrowser('' + child) +
376
+ '<!-- /react-text -->'
377
+ );
378
+ } else {
379
+ ({child, context} = resolve(child, context));
380
+ if (child === null || child === false) {
381
+ return '<!-- react-empty: ' + this.idCounter++ + ' -->';
382
+ } else {
383
+ return this.renderDOM(child, context);
384
+ }
385
+ }
386
+ }
387
+
388
+ renderDOM(element, context) {
389
+ var tag = element.type.toLowerCase();
390
+
391
+ if (__DEV__) {
392
+ warning(
393
+ tag === element.type,
394
+ '<%s /> is using uppercase HTML. Always use lowercase HTML tags ' +
395
+ 'in React.',
396
+ element.type,
397
+ );
398
+ }
399
+
400
+ validateDangerousTag(tag);
401
+
402
+ var props = element.props;
403
+ if (tag === 'input') {
404
+ if (__DEV__) {
405
+ ReactControlledValuePropTypes.checkPropTypes(
406
+ 'input',
407
+ props,
408
+ () => '', //getCurrentFiberStackAddendum
409
+ );
410
+
411
+ if (
412
+ props.checked !== undefined &&
413
+ props.defaultChecked !== undefined &&
414
+ !didWarnDefaultChecked
415
+ ) {
416
+ warning(
417
+ false,
418
+ '%s contains an input of type %s with both checked and defaultChecked props. ' +
419
+ 'Input elements must be either controlled or uncontrolled ' +
420
+ '(specify either the checked prop, or the defaultChecked prop, but not ' +
421
+ 'both). Decide between using a controlled or uncontrolled input ' +
422
+ 'element and remove one of these props. More info: ' +
423
+ 'https://fb.me/react-controlled-components',
424
+ 'A component',
425
+ props.type,
426
+ );
427
+ didWarnDefaultChecked = true;
428
+ }
429
+ if (
430
+ props.value !== undefined &&
431
+ props.defaultValue !== undefined &&
432
+ !didWarnDefaultInputValue
433
+ ) {
434
+ warning(
435
+ false,
436
+ '%s contains an input of type %s with both value and defaultValue props. ' +
437
+ 'Input elements must be either controlled or uncontrolled ' +
438
+ '(specify either the value prop, or the defaultValue prop, but not ' +
439
+ 'both). Decide between using a controlled or uncontrolled input ' +
440
+ 'element and remove one of these props. More info: ' +
441
+ 'https://fb.me/react-controlled-components',
442
+ 'A component',
443
+ props.type,
444
+ );
445
+ didWarnDefaultInputValue = true;
446
+ }
447
+ }
448
+
449
+ props = Object.assign(
450
+ {
451
+ type: undefined,
452
+ },
453
+ props,
454
+ {
455
+ defaultChecked: undefined,
456
+ defaultValue: undefined,
457
+ value: props.value != null ? props.value : props.defaultValue,
458
+ checked: props.checked != null ? props.checked : props.defaultChecked,
459
+ },
460
+ );
461
+ } else if (tag === 'textarea') {
462
+ if (__DEV__) {
463
+ ReactControlledValuePropTypes.checkPropTypes(
464
+ 'textarea',
465
+ props,
466
+ () => '', //getCurrentFiberStackAddendum
467
+ );
468
+ if (
469
+ props.value !== undefined &&
470
+ props.defaultValue !== undefined &&
471
+ !didWarnDefaultTextareaValue
472
+ ) {
473
+ warning(
474
+ false,
475
+ 'Textarea elements must be either controlled or uncontrolled ' +
476
+ '(specify either the value prop, or the defaultValue prop, but not ' +
477
+ 'both). Decide between using a controlled or uncontrolled textarea ' +
478
+ 'and remove one of these props. More info: ' +
479
+ 'https://fb.me/react-controlled-components',
480
+ );
481
+ didWarnDefaultTextareaValue = true;
482
+ }
483
+ }
484
+
485
+ var initialValue = props.value;
486
+ if (initialValue == null) {
487
+ var defaultValue = props.defaultValue;
488
+ // TODO (yungsters): Remove support for children content in <textarea>.
489
+ var textareaChildren = props.children;
490
+ if (textareaChildren != null) {
491
+ if (__DEV__) {
492
+ warning(
493
+ false,
494
+ 'Use the `defaultValue` or `value` props instead of setting ' +
495
+ 'children on <textarea>.',
496
+ );
497
+ }
498
+ invariant(
499
+ defaultValue == null,
500
+ 'If you supply `defaultValue` on a <textarea>, do not pass children.',
501
+ );
502
+ if (Array.isArray(textareaChildren)) {
503
+ invariant(
504
+ textareaChildren.length <= 1,
505
+ '<textarea> can only have at most one child.',
506
+ );
507
+ textareaChildren = textareaChildren[0];
508
+ }
509
+
510
+ defaultValue = '' + textareaChildren;
511
+ }
512
+ if (defaultValue == null) {
513
+ defaultValue = '';
514
+ }
515
+ initialValue = defaultValue;
516
+ }
517
+
518
+ props = Object.assign({}, props, {
519
+ value: undefined,
520
+ children: '' + initialValue,
521
+ });
522
+ } else if (tag === 'select') {
523
+ if (__DEV__) {
524
+ ReactControlledValuePropTypes.checkPropTypes(
525
+ 'select',
526
+ props,
527
+ () => '', // getCurrentFiberStackAddendum,
528
+ );
529
+
530
+ for (var i = 0; i < valuePropNames.length; i++) {
531
+ var propName = valuePropNames[i];
532
+ if (props[propName] == null) {
533
+ continue;
534
+ }
535
+ var isArray = Array.isArray(props[propName]);
536
+ if (props.multiple && !isArray) {
537
+ warning(
538
+ false,
539
+ 'The `%s` prop supplied to <select> must be an array if ' +
540
+ '`multiple` is true.%s',
541
+ propName,
542
+ '', // getDeclarationErrorAddendum(),
543
+ );
544
+ } else if (!props.multiple && isArray) {
545
+ warning(
546
+ false,
547
+ 'The `%s` prop supplied to <select> must be a scalar ' +
548
+ 'value if `multiple` is false.%s',
549
+ propName,
550
+ '', // getDeclarationErrorAddendum(),
551
+ );
552
+ }
553
+ }
554
+ }
555
+
556
+ if (
557
+ props.value !== undefined &&
558
+ props.defaultValue !== undefined &&
559
+ !didWarnDefaultSelectValue
560
+ ) {
561
+ warning(
562
+ false,
563
+ 'Select elements must be either controlled or uncontrolled ' +
564
+ '(specify either the value prop, or the defaultValue prop, but not ' +
565
+ 'both). Decide between using a controlled or uncontrolled select ' +
566
+ 'element and remove one of these props. More info: ' +
567
+ 'https://fb.me/react-controlled-components',
568
+ );
569
+ didWarnDefaultSelectValue = true;
570
+ }
571
+
572
+ this.currentSelectValue = props.value != null
573
+ ? props.value
574
+ : props.defaultValue;
575
+ props = Object.assign({}, props, {
576
+ value: undefined,
577
+ });
578
+ } else if (tag === 'option') {
579
+ var selected = null;
580
+ var selectValue = this.currentSelectValue;
581
+ var optionChildren = flattenOptionChildren(props.children);
582
+ if (selectValue != null) {
583
+ var value;
584
+ if (props.value != null) {
585
+ value = props.value + '';
586
+ } else {
587
+ value = optionChildren;
588
+ }
589
+ selected = false;
590
+ if (Array.isArray(selectValue)) {
591
+ // multiple
592
+ for (var j = 0; j < selectValue.length; j++) {
593
+ if ('' + selectValue[j] === value) {
594
+ selected = true;
595
+ break;
596
+ }
597
+ }
598
+ } else {
599
+ selected = '' + selectValue === value;
600
+ }
601
+
602
+ props = Object.assign(
603
+ {
604
+ selected: undefined,
605
+ children: undefined,
606
+ },
607
+ props,
608
+ {
609
+ selected: selected,
610
+ children: optionChildren,
611
+ },
612
+ );
613
+ }
614
+ }
615
+
616
+ if (__DEV__) {
617
+ validatePropertiesInDevelopment(tag, props);
618
+ }
619
+
620
+ assertValidProps(tag, props);
621
+
622
+ var out = createOpenTagMarkup(
623
+ element.type,
624
+ tag,
625
+ props,
626
+ this.makeStaticMarkup,
627
+ this.stack.length === 1,
628
+ this.idCounter++,
629
+ null,
630
+ );
631
+ var footer = '';
632
+ if (omittedCloseTags.hasOwnProperty(tag)) {
633
+ out += '/>';
634
+ } else {
635
+ out += '>';
636
+ footer = '</' + element.type + '>';
637
+ }
638
+ var children = [];
639
+ var innerMarkup = getNonChildrenInnerMarkup(props);
640
+ if (innerMarkup != null) {
641
+ if (newlineEatingTags[tag] && innerMarkup.charAt(0) === '\n') {
642
+ // text/html ignores the first character in these tags if it's a newline
643
+ // Prefer to break application/xml over text/html (for now) by adding
644
+ // a newline specifically to get eaten by the parser. (Alternately for
645
+ // textareas, replacing "^\n" with "\r\n" doesn't get eaten, and the first
646
+ // \r is normalized out by HTMLTextAreaElement#value.)
647
+ // See: <http://www.w3.org/TR/html-polyglot/#newlines-in-textarea-and-pre>
648
+ // See: <http://www.w3.org/TR/html5/syntax.html#element-restrictions>
649
+ // See: <http://www.w3.org/TR/html5/syntax.html#newlines>
650
+ // See: Parsing of "textarea" "listing" and "pre" elements
651
+ // from <http://www.w3.org/TR/html5/syntax.html#parsing-main-inbody>
652
+ out += '\n';
653
+ }
654
+ out += innerMarkup;
655
+ } else {
656
+ traverseAllChildren(props.children, function(ctx, child, name) {
657
+ if (child != null) {
658
+ children.push(child);
659
+ }
660
+ });
661
+ }
662
+ this.stack.push({
663
+ tag,
664
+ children,
665
+ childIndex: 0,
666
+ context: context,
667
+ footer: footer,
668
+ });
669
+ return out;
670
+ }
671
+}
672
+
673
+/**
674
+ * Render a ReactElement to its initial HTML. This should only be used on the
675
+ * server.
676
+ * See https://facebook.github.io/react/docs/react-dom-server.html#rendertostring
677
+ */
678
+function renderToString(element) {
679
+ invariant(
680
+ ReactElement.isValidElement(element),
681
+ 'renderToString(): You must pass a valid ReactElement.',
682
+ );
683
+ var renderer = new ReactDOMServerRenderer(element, false);
684
+ var markup = renderer.read(Infinity);
685
+ markup = ReactMarkupChecksum.addChecksumToMarkup(markup);
686
+ return markup;
687
+}
688
+
689
+/**
690
+ * Similar to renderToString, except this doesn't create extra DOM attributes
691
+ * such as data-react-id that React uses internally.
692
+ * See https://facebook.github.io/react/docs/react-dom-server.html#rendertostaticmarkup
693
+ */
694
+function renderToStaticMarkup(element) {
695
+ invariant(
696
+ ReactElement.isValidElement(element),
697
+ 'renderToStaticMarkup(): You must pass a valid ReactElement.',
698
+ );
699
+ var renderer = new ReactDOMServerRenderer(element, true);
700
+ var markup = renderer.read(Infinity);
701
+ return markup;
702
+}
703
+
704
+var ReactServerRenderer = {
705
+ renderToString: renderToString,
706
+ renderToStaticMarkup: renderToStaticMarkup,
707
+};
708
+
709
+module.exports = ReactServerRenderer;