react-jsx-visitors.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  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. 'use strict';
  10. /*global exports:true*/
  11. var Syntax = require('esprima-fb').Syntax;
  12. var utils = require('../src/utils');
  13. var jsxHelpers = require('./jsx-helpers');
  14. var renderJSXExpressionContainer = jsxHelpers.renderJSXExpressionContainer;
  15. var renderJSXLiteral = jsxHelpers.renderJSXLiteral;
  16. var quoteAttrName = jsxHelpers.quoteAttrName;
  17. var trimLeft = jsxHelpers.trimLeft;
  18. /**
  19. * Customized desugar processor for React JSX. Currently:
  20. *
  21. * <X> </X> => React.createElement(X, null)
  22. * <X prop="1" /> => React.createElement(X, {prop: '1'}, null)
  23. * <X prop="2"><Y /></X> => React.createElement(X, {prop:'2'},
  24. * React.createElement(Y, null)
  25. * )
  26. * <div /> => React.createElement("div", null)
  27. */
  28. /**
  29. * Removes all non-whitespace/parenthesis characters
  30. */
  31. var reNonWhiteParen = /([^\s\(\)])/g;
  32. function stripNonWhiteParen(value) {
  33. return value.replace(reNonWhiteParen, '');
  34. }
  35. var tagConvention = /^[a-z]|\-/;
  36. function isTagName(name) {
  37. return tagConvention.test(name);
  38. }
  39. function visitReactTag(traverse, object, path, state) {
  40. var openingElement = object.openingElement;
  41. var nameObject = openingElement.name;
  42. var attributesObject = openingElement.attributes;
  43. utils.catchup(openingElement.range[0], state, trimLeft);
  44. if (nameObject.type === Syntax.JSXNamespacedName && nameObject.namespace) {
  45. throw new Error('Namespace tags are not supported. ReactJSX is not XML.');
  46. }
  47. // We assume that the React runtime is already in scope
  48. utils.append('React.createElement(', state);
  49. if (nameObject.type === Syntax.JSXIdentifier && isTagName(nameObject.name)) {
  50. utils.append('"' + nameObject.name + '"', state);
  51. utils.move(nameObject.range[1], state);
  52. } else {
  53. // Use utils.catchup in this case so we can easily handle
  54. // JSXMemberExpressions which look like Foo.Bar.Baz. This also handles
  55. // JSXIdentifiers that aren't fallback tags.
  56. utils.move(nameObject.range[0], state);
  57. utils.catchup(nameObject.range[1], state);
  58. }
  59. utils.append(', ', state);
  60. var hasAttributes = attributesObject.length;
  61. var hasAtLeastOneSpreadProperty = attributesObject.some(function(attr) {
  62. return attr.type === Syntax.JSXSpreadAttribute;
  63. });
  64. // if we don't have any attributes, pass in null
  65. if (hasAtLeastOneSpreadProperty) {
  66. utils.append('React.__spread({', state);
  67. } else if (hasAttributes) {
  68. utils.append('{', state);
  69. } else {
  70. utils.append('null', state);
  71. }
  72. // keep track of if the previous attribute was a spread attribute
  73. var previousWasSpread = false;
  74. // write attributes
  75. attributesObject.forEach(function(attr, index) {
  76. var isLast = index === attributesObject.length - 1;
  77. if (attr.type === Syntax.JSXSpreadAttribute) {
  78. // Close the previous object or initial object
  79. if (!previousWasSpread) {
  80. utils.append('}, ', state);
  81. }
  82. // Move to the expression start, ignoring everything except parenthesis
  83. // and whitespace.
  84. utils.catchup(attr.range[0], state, stripNonWhiteParen);
  85. // Plus 1 to skip `{`.
  86. utils.move(attr.range[0] + 1, state);
  87. utils.catchup(attr.argument.range[0], state, stripNonWhiteParen);
  88. traverse(attr.argument, path, state);
  89. utils.catchup(attr.argument.range[1], state);
  90. // Move to the end, ignoring parenthesis and the closing `}`
  91. utils.catchup(attr.range[1] - 1, state, stripNonWhiteParen);
  92. if (!isLast) {
  93. utils.append(', ', state);
  94. }
  95. utils.move(attr.range[1], state);
  96. previousWasSpread = true;
  97. return;
  98. }
  99. // If the next attribute is a spread, we're effective last in this object
  100. if (!isLast) {
  101. isLast = attributesObject[index + 1].type === Syntax.JSXSpreadAttribute;
  102. }
  103. if (attr.name.namespace) {
  104. throw new Error(
  105. 'Namespace attributes are not supported. ReactJSX is not XML.');
  106. }
  107. var name = attr.name.name;
  108. utils.catchup(attr.range[0], state, trimLeft);
  109. if (previousWasSpread) {
  110. utils.append('{', state);
  111. }
  112. utils.append(quoteAttrName(name), state);
  113. utils.append(': ', state);
  114. if (!attr.value) {
  115. state.g.buffer += 'true';
  116. state.g.position = attr.name.range[1];
  117. if (!isLast) {
  118. utils.append(', ', state);
  119. }
  120. } else {
  121. utils.move(attr.name.range[1], state);
  122. // Use catchupNewlines to skip over the '=' in the attribute
  123. utils.catchupNewlines(attr.value.range[0], state);
  124. if (attr.value.type === Syntax.Literal) {
  125. renderJSXLiteral(attr.value, isLast, state);
  126. } else {
  127. renderJSXExpressionContainer(traverse, attr.value, isLast, path, state);
  128. }
  129. }
  130. utils.catchup(attr.range[1], state, trimLeft);
  131. previousWasSpread = false;
  132. });
  133. if (!openingElement.selfClosing) {
  134. utils.catchup(openingElement.range[1] - 1, state, trimLeft);
  135. utils.move(openingElement.range[1], state);
  136. }
  137. if (hasAttributes && !previousWasSpread) {
  138. utils.append('}', state);
  139. }
  140. if (hasAtLeastOneSpreadProperty) {
  141. utils.append(')', state);
  142. }
  143. // filter out whitespace
  144. var childrenToRender = object.children.filter(function(child) {
  145. return !(child.type === Syntax.Literal
  146. && typeof child.value === 'string'
  147. && child.value.match(/^[ \t]*[\r\n][ \t\r\n]*$/));
  148. });
  149. if (childrenToRender.length > 0) {
  150. var lastRenderableIndex;
  151. childrenToRender.forEach(function(child, index) {
  152. if (child.type !== Syntax.JSXExpressionContainer ||
  153. child.expression.type !== Syntax.JSXEmptyExpression) {
  154. lastRenderableIndex = index;
  155. }
  156. });
  157. if (lastRenderableIndex !== undefined) {
  158. utils.append(', ', state);
  159. }
  160. childrenToRender.forEach(function(child, index) {
  161. utils.catchup(child.range[0], state, trimLeft);
  162. var isLast = index >= lastRenderableIndex;
  163. if (child.type === Syntax.Literal) {
  164. renderJSXLiteral(child, isLast, state);
  165. } else if (child.type === Syntax.JSXExpressionContainer) {
  166. renderJSXExpressionContainer(traverse, child, isLast, path, state);
  167. } else {
  168. traverse(child, path, state);
  169. if (!isLast) {
  170. utils.append(', ', state);
  171. }
  172. }
  173. utils.catchup(child.range[1], state, trimLeft);
  174. });
  175. }
  176. if (openingElement.selfClosing) {
  177. // everything up to />
  178. utils.catchup(openingElement.range[1] - 2, state, trimLeft);
  179. utils.move(openingElement.range[1], state);
  180. } else {
  181. // everything up to </ sdflksjfd>
  182. utils.catchup(object.closingElement.range[0], state, trimLeft);
  183. utils.move(object.closingElement.range[1], state);
  184. }
  185. utils.append(')', state);
  186. return false;
  187. }
  188. visitReactTag.test = function(object, path, state) {
  189. return object.type === Syntax.JSXElement;
  190. };
  191. exports.visitorList = [
  192. visitReactTag
  193. ];