index.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. import {
  2. eat,
  3. finishToken,
  4. getTokenFromCode,
  5. IdentifierRole,
  6. JSXRole,
  7. match,
  8. next,
  9. skipSpace,
  10. Token,
  11. } from "../../tokenizer/index";
  12. import {TokenType as tt} from "../../tokenizer/types";
  13. import {input, isTypeScriptEnabled, state} from "../../traverser/base";
  14. import {parseExpression, parseMaybeAssign} from "../../traverser/expression";
  15. import {expect, unexpected} from "../../traverser/util";
  16. import {charCodes} from "../../util/charcodes";
  17. import {IS_IDENTIFIER_CHAR, IS_IDENTIFIER_START} from "../../util/identifier";
  18. import {tsTryParseJSXTypeArgument} from "../typescript";
  19. /**
  20. * Read token with JSX contents.
  21. *
  22. * In addition to detecting jsxTagStart and also regular tokens that might be
  23. * part of an expression, this code detects the start and end of text ranges
  24. * within JSX children. In order to properly count the number of children, we
  25. * distinguish jsxText from jsxEmptyText, which is a text range that simplifies
  26. * to the empty string after JSX whitespace trimming.
  27. *
  28. * It turns out that a JSX text range will simplify to the empty string if and
  29. * only if both of these conditions hold:
  30. * - The range consists entirely of whitespace characters (only counting space,
  31. * tab, \r, and \n).
  32. * - The range has at least one newline.
  33. * This can be proven by analyzing any implementation of whitespace trimming,
  34. * e.g. formatJSXTextLiteral in Sucrase or cleanJSXElementLiteralChild in Babel.
  35. */
  36. function jsxReadToken() {
  37. let sawNewline = false;
  38. let sawNonWhitespace = false;
  39. while (true) {
  40. if (state.pos >= input.length) {
  41. unexpected("Unterminated JSX contents");
  42. return;
  43. }
  44. const ch = input.charCodeAt(state.pos);
  45. if (ch === charCodes.lessThan || ch === charCodes.leftCurlyBrace) {
  46. if (state.pos === state.start) {
  47. if (ch === charCodes.lessThan) {
  48. state.pos++;
  49. finishToken(tt.jsxTagStart);
  50. return;
  51. }
  52. getTokenFromCode(ch);
  53. return;
  54. }
  55. if (sawNewline && !sawNonWhitespace) {
  56. finishToken(tt.jsxEmptyText);
  57. } else {
  58. finishToken(tt.jsxText);
  59. }
  60. return;
  61. }
  62. // This is part of JSX text.
  63. if (ch === charCodes.lineFeed) {
  64. sawNewline = true;
  65. } else if (ch !== charCodes.space && ch !== charCodes.carriageReturn && ch !== charCodes.tab) {
  66. sawNonWhitespace = true;
  67. }
  68. state.pos++;
  69. }
  70. }
  71. function jsxReadString(quote) {
  72. state.pos++;
  73. for (;;) {
  74. if (state.pos >= input.length) {
  75. unexpected("Unterminated string constant");
  76. return;
  77. }
  78. const ch = input.charCodeAt(state.pos);
  79. if (ch === quote) {
  80. state.pos++;
  81. break;
  82. }
  83. state.pos++;
  84. }
  85. finishToken(tt.string);
  86. }
  87. // Read a JSX identifier (valid tag or attribute name).
  88. //
  89. // Optimized version since JSX identifiers can't contain
  90. // escape characters and so can be read as single slice.
  91. // Also assumes that first character was already checked
  92. // by isIdentifierStart in readToken.
  93. function jsxReadWord() {
  94. let ch;
  95. do {
  96. if (state.pos > input.length) {
  97. unexpected("Unexpectedly reached the end of input.");
  98. return;
  99. }
  100. ch = input.charCodeAt(++state.pos);
  101. } while (IS_IDENTIFIER_CHAR[ch] || ch === charCodes.dash);
  102. finishToken(tt.jsxName);
  103. }
  104. // Parse next token as JSX identifier
  105. function jsxParseIdentifier() {
  106. nextJSXTagToken();
  107. }
  108. // Parse namespaced identifier.
  109. function jsxParseNamespacedName(identifierRole) {
  110. jsxParseIdentifier();
  111. if (!eat(tt.colon)) {
  112. // Plain identifier, so this is an access.
  113. state.tokens[state.tokens.length - 1].identifierRole = identifierRole;
  114. return;
  115. }
  116. // Process the second half of the namespaced name.
  117. jsxParseIdentifier();
  118. }
  119. // Parses element name in any form - namespaced, member
  120. // or single identifier.
  121. function jsxParseElementName() {
  122. const firstTokenIndex = state.tokens.length;
  123. jsxParseNamespacedName(IdentifierRole.Access);
  124. let hadDot = false;
  125. while (match(tt.dot)) {
  126. hadDot = true;
  127. nextJSXTagToken();
  128. jsxParseIdentifier();
  129. }
  130. // For tags like <div> with a lowercase letter and no dots, the name is
  131. // actually *not* an identifier access, since it's referring to a built-in
  132. // tag name. Remove the identifier role in this case so that it's not
  133. // accidentally transformed by the imports transform when preserving JSX.
  134. if (!hadDot) {
  135. const firstToken = state.tokens[firstTokenIndex];
  136. const firstChar = input.charCodeAt(firstToken.start);
  137. if (firstChar >= charCodes.lowercaseA && firstChar <= charCodes.lowercaseZ) {
  138. firstToken.identifierRole = null;
  139. }
  140. }
  141. }
  142. // Parses any type of JSX attribute value.
  143. function jsxParseAttributeValue() {
  144. switch (state.type) {
  145. case tt.braceL:
  146. next();
  147. parseExpression();
  148. nextJSXTagToken();
  149. return;
  150. case tt.jsxTagStart:
  151. jsxParseElement();
  152. nextJSXTagToken();
  153. return;
  154. case tt.string:
  155. nextJSXTagToken();
  156. return;
  157. default:
  158. unexpected("JSX value should be either an expression or a quoted JSX text");
  159. }
  160. }
  161. // Parse JSX spread child, after already processing the {
  162. // Does not parse the closing }
  163. function jsxParseSpreadChild() {
  164. expect(tt.ellipsis);
  165. parseExpression();
  166. }
  167. // Parses JSX opening tag starting after "<".
  168. // Returns true if the tag was self-closing.
  169. // Does not parse the last token.
  170. function jsxParseOpeningElement(initialTokenIndex) {
  171. if (match(tt.jsxTagEnd)) {
  172. // This is an open-fragment.
  173. return false;
  174. }
  175. jsxParseElementName();
  176. if (isTypeScriptEnabled) {
  177. tsTryParseJSXTypeArgument();
  178. }
  179. let hasSeenPropSpread = false;
  180. while (!match(tt.slash) && !match(tt.jsxTagEnd) && !state.error) {
  181. if (eat(tt.braceL)) {
  182. hasSeenPropSpread = true;
  183. expect(tt.ellipsis);
  184. parseMaybeAssign();
  185. // }
  186. nextJSXTagToken();
  187. continue;
  188. }
  189. if (
  190. hasSeenPropSpread &&
  191. state.end - state.start === 3 &&
  192. input.charCodeAt(state.start) === charCodes.lowercaseK &&
  193. input.charCodeAt(state.start + 1) === charCodes.lowercaseE &&
  194. input.charCodeAt(state.start + 2) === charCodes.lowercaseY
  195. ) {
  196. state.tokens[initialTokenIndex].jsxRole = JSXRole.KeyAfterPropSpread;
  197. }
  198. jsxParseNamespacedName(IdentifierRole.ObjectKey);
  199. if (match(tt.eq)) {
  200. nextJSXTagToken();
  201. jsxParseAttributeValue();
  202. }
  203. }
  204. const isSelfClosing = match(tt.slash);
  205. if (isSelfClosing) {
  206. // /
  207. nextJSXTagToken();
  208. }
  209. return isSelfClosing;
  210. }
  211. // Parses JSX closing tag starting after "</".
  212. // Does not parse the last token.
  213. function jsxParseClosingElement() {
  214. if (match(tt.jsxTagEnd)) {
  215. // Fragment syntax, so we immediately have a tag end.
  216. return;
  217. }
  218. jsxParseElementName();
  219. }
  220. // Parses entire JSX element, including its opening tag
  221. // (starting after "<"), attributes, contents and closing tag.
  222. // Does not parse the last token.
  223. function jsxParseElementAt() {
  224. const initialTokenIndex = state.tokens.length - 1;
  225. state.tokens[initialTokenIndex].jsxRole = JSXRole.NoChildren;
  226. let numExplicitChildren = 0;
  227. const isSelfClosing = jsxParseOpeningElement(initialTokenIndex);
  228. if (!isSelfClosing) {
  229. nextJSXExprToken();
  230. while (true) {
  231. switch (state.type) {
  232. case tt.jsxTagStart:
  233. nextJSXTagToken();
  234. if (match(tt.slash)) {
  235. nextJSXTagToken();
  236. jsxParseClosingElement();
  237. // Key after prop spread takes precedence over number of children,
  238. // since it means we switch to createElement, which doesn't care
  239. // about number of children.
  240. if (state.tokens[initialTokenIndex].jsxRole !== JSXRole.KeyAfterPropSpread) {
  241. if (numExplicitChildren === 1) {
  242. state.tokens[initialTokenIndex].jsxRole = JSXRole.OneChild;
  243. } else if (numExplicitChildren > 1) {
  244. state.tokens[initialTokenIndex].jsxRole = JSXRole.StaticChildren;
  245. }
  246. }
  247. return;
  248. }
  249. numExplicitChildren++;
  250. jsxParseElementAt();
  251. nextJSXExprToken();
  252. break;
  253. case tt.jsxText:
  254. numExplicitChildren++;
  255. nextJSXExprToken();
  256. break;
  257. case tt.jsxEmptyText:
  258. nextJSXExprToken();
  259. break;
  260. case tt.braceL:
  261. next();
  262. if (match(tt.ellipsis)) {
  263. jsxParseSpreadChild();
  264. nextJSXExprToken();
  265. // Spread children are a mechanism to explicitly mark children as
  266. // static, so count it as 2 children to satisfy the "more than one
  267. // child" condition.
  268. numExplicitChildren += 2;
  269. } else {
  270. // If we see {}, this is an empty pseudo-expression that doesn't
  271. // count as a child.
  272. if (!match(tt.braceR)) {
  273. numExplicitChildren++;
  274. parseExpression();
  275. }
  276. nextJSXExprToken();
  277. }
  278. break;
  279. // istanbul ignore next - should never happen
  280. default:
  281. unexpected();
  282. return;
  283. }
  284. }
  285. }
  286. }
  287. // Parses entire JSX element from current position.
  288. // Does not parse the last token.
  289. export function jsxParseElement() {
  290. nextJSXTagToken();
  291. jsxParseElementAt();
  292. }
  293. // ==================================
  294. // Overrides
  295. // ==================================
  296. export function nextJSXTagToken() {
  297. state.tokens.push(new Token());
  298. skipSpace();
  299. state.start = state.pos;
  300. const code = input.charCodeAt(state.pos);
  301. if (IS_IDENTIFIER_START[code]) {
  302. jsxReadWord();
  303. } else if (code === charCodes.quotationMark || code === charCodes.apostrophe) {
  304. jsxReadString(code);
  305. } else {
  306. // The following tokens are just one character each.
  307. ++state.pos;
  308. switch (code) {
  309. case charCodes.greaterThan:
  310. finishToken(tt.jsxTagEnd);
  311. break;
  312. case charCodes.lessThan:
  313. finishToken(tt.jsxTagStart);
  314. break;
  315. case charCodes.slash:
  316. finishToken(tt.slash);
  317. break;
  318. case charCodes.equalsTo:
  319. finishToken(tt.eq);
  320. break;
  321. case charCodes.leftCurlyBrace:
  322. finishToken(tt.braceL);
  323. break;
  324. case charCodes.dot:
  325. finishToken(tt.dot);
  326. break;
  327. case charCodes.colon:
  328. finishToken(tt.colon);
  329. break;
  330. default:
  331. unexpected();
  332. }
  333. }
  334. }
  335. function nextJSXExprToken() {
  336. state.tokens.push(new Token());
  337. state.start = state.pos;
  338. jsxReadToken();
  339. }