You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

488 lines
15 KiB

4 years ago
  1. 'use strict';
  2. const XHTMLEntities = require('./xhtml');
  3. const hexNumber = /^[\da-fA-F]+$/;
  4. const decimalNumber = /^\d+$/;
  5. // The map to `acorn-jsx` tokens from `acorn` namespace objects.
  6. const acornJsxMap = new WeakMap();
  7. // Get the original tokens for the given `acorn` namespace object.
  8. function getJsxTokens(acorn) {
  9. acorn = acorn.Parser.acorn || acorn;
  10. let acornJsx = acornJsxMap.get(acorn);
  11. if (!acornJsx) {
  12. const tt = acorn.tokTypes;
  13. const TokContext = acorn.TokContext;
  14. const TokenType = acorn.TokenType;
  15. const tc_oTag = new TokContext('<tag', false);
  16. const tc_cTag = new TokContext('</tag', false);
  17. const tc_expr = new TokContext('<tag>...</tag>', true, true);
  18. const tokContexts = {
  19. tc_oTag: tc_oTag,
  20. tc_cTag: tc_cTag,
  21. tc_expr: tc_expr
  22. };
  23. const tokTypes = {
  24. jsxName: new TokenType('jsxName'),
  25. jsxText: new TokenType('jsxText', {beforeExpr: true}),
  26. jsxTagStart: new TokenType('jsxTagStart'),
  27. jsxTagEnd: new TokenType('jsxTagEnd')
  28. };
  29. tokTypes.jsxTagStart.updateContext = function() {
  30. this.context.push(tc_expr); // treat as beginning of JSX expression
  31. this.context.push(tc_oTag); // start opening tag context
  32. this.exprAllowed = false;
  33. };
  34. tokTypes.jsxTagEnd.updateContext = function(prevType) {
  35. let out = this.context.pop();
  36. if (out === tc_oTag && prevType === tt.slash || out === tc_cTag) {
  37. this.context.pop();
  38. this.exprAllowed = this.curContext() === tc_expr;
  39. } else {
  40. this.exprAllowed = true;
  41. }
  42. };
  43. acornJsx = { tokContexts: tokContexts, tokTypes: tokTypes };
  44. acornJsxMap.set(acorn, acornJsx);
  45. }
  46. return acornJsx;
  47. }
  48. // Transforms JSX element name to string.
  49. function getQualifiedJSXName(object) {
  50. if (!object)
  51. return object;
  52. if (object.type === 'JSXIdentifier')
  53. return object.name;
  54. if (object.type === 'JSXNamespacedName')
  55. return object.namespace.name + ':' + object.name.name;
  56. if (object.type === 'JSXMemberExpression')
  57. return getQualifiedJSXName(object.object) + '.' +
  58. getQualifiedJSXName(object.property);
  59. }
  60. module.exports = function(options) {
  61. options = options || {};
  62. return function(Parser) {
  63. return plugin({
  64. allowNamespaces: options.allowNamespaces !== false,
  65. allowNamespacedObjects: !!options.allowNamespacedObjects
  66. }, Parser);
  67. };
  68. };
  69. // This is `tokTypes` of the peer dep.
  70. // This can be different instances from the actual `tokTypes` this plugin uses.
  71. Object.defineProperty(module.exports, "tokTypes", {
  72. get: function get_tokTypes() {
  73. return getJsxTokens(require("acorn")).tokTypes;
  74. },
  75. configurable: true,
  76. enumerable: true
  77. });
  78. function plugin(options, Parser) {
  79. const acorn = Parser.acorn || require("acorn");
  80. const acornJsx = getJsxTokens(acorn);
  81. const tt = acorn.tokTypes;
  82. const tok = acornJsx.tokTypes;
  83. const tokContexts = acorn.tokContexts;
  84. const tc_oTag = acornJsx.tokContexts.tc_oTag;
  85. const tc_cTag = acornJsx.tokContexts.tc_cTag;
  86. const tc_expr = acornJsx.tokContexts.tc_expr;
  87. const isNewLine = acorn.isNewLine;
  88. const isIdentifierStart = acorn.isIdentifierStart;
  89. const isIdentifierChar = acorn.isIdentifierChar;
  90. return class extends Parser {
  91. // Expose actual `tokTypes` and `tokContexts` to other plugins.
  92. static get acornJsx() {
  93. return acornJsx;
  94. }
  95. // Reads inline JSX contents token.
  96. jsx_readToken() {
  97. let out = '', chunkStart = this.pos;
  98. for (;;) {
  99. if (this.pos >= this.input.length)
  100. this.raise(this.start, 'Unterminated JSX contents');
  101. let ch = this.input.charCodeAt(this.pos);
  102. switch (ch) {
  103. case 60: // '<'
  104. case 123: // '{'
  105. if (this.pos === this.start) {
  106. if (ch === 60 && this.exprAllowed) {
  107. ++this.pos;
  108. return this.finishToken(tok.jsxTagStart);
  109. }
  110. return this.getTokenFromCode(ch);
  111. }
  112. out += this.input.slice(chunkStart, this.pos);
  113. return this.finishToken(tok.jsxText, out);
  114. case 38: // '&'
  115. out += this.input.slice(chunkStart, this.pos);
  116. out += this.jsx_readEntity();
  117. chunkStart = this.pos;
  118. break;
  119. case 62: // '>'
  120. case 125: // '}'
  121. this.raise(
  122. this.pos,
  123. "Unexpected token `" + this.input[this.pos] + "`. Did you mean `" +
  124. (ch === 62 ? "&gt;" : "&rbrace;") + "` or " + "`{\"" + this.input[this.pos] + "\"}" + "`?"
  125. );
  126. default:
  127. if (isNewLine(ch)) {
  128. out += this.input.slice(chunkStart, this.pos);
  129. out += this.jsx_readNewLine(true);
  130. chunkStart = this.pos;
  131. } else {
  132. ++this.pos;
  133. }
  134. }
  135. }
  136. }
  137. jsx_readNewLine(normalizeCRLF) {
  138. let ch = this.input.charCodeAt(this.pos);
  139. let out;
  140. ++this.pos;
  141. if (ch === 13 && this.input.charCodeAt(this.pos) === 10) {
  142. ++this.pos;
  143. out = normalizeCRLF ? '\n' : '\r\n';
  144. } else {
  145. out = String.fromCharCode(ch);
  146. }
  147. if (this.options.locations) {
  148. ++this.curLine;
  149. this.lineStart = this.pos;
  150. }
  151. return out;
  152. }
  153. jsx_readString(quote) {
  154. let out = '', chunkStart = ++this.pos;
  155. for (;;) {
  156. if (this.pos >= this.input.length)
  157. this.raise(this.start, 'Unterminated string constant');
  158. let ch = this.input.charCodeAt(this.pos);
  159. if (ch === quote) break;
  160. if (ch === 38) { // '&'
  161. out += this.input.slice(chunkStart, this.pos);
  162. out += this.jsx_readEntity();
  163. chunkStart = this.pos;
  164. } else if (isNewLine(ch)) {
  165. out += this.input.slice(chunkStart, this.pos);
  166. out += this.jsx_readNewLine(false);
  167. chunkStart = this.pos;
  168. } else {
  169. ++this.pos;
  170. }
  171. }
  172. out += this.input.slice(chunkStart, this.pos++);
  173. return this.finishToken(tt.string, out);
  174. }
  175. jsx_readEntity() {
  176. let str = '', count = 0, entity;
  177. let ch = this.input[this.pos];
  178. if (ch !== '&')
  179. this.raise(this.pos, 'Entity must start with an ampersand');
  180. let startPos = ++this.pos;
  181. while (this.pos < this.input.length && count++ < 10) {
  182. ch = this.input[this.pos++];
  183. if (ch === ';') {
  184. if (str[0] === '#') {
  185. if (str[1] === 'x') {
  186. str = str.substr(2);
  187. if (hexNumber.test(str))
  188. entity = String.fromCharCode(parseInt(str, 16));
  189. } else {
  190. str = str.substr(1);
  191. if (decimalNumber.test(str))
  192. entity = String.fromCharCode(parseInt(str, 10));
  193. }
  194. } else {
  195. entity = XHTMLEntities[str];
  196. }
  197. break;
  198. }
  199. str += ch;
  200. }
  201. if (!entity) {
  202. this.pos = startPos;
  203. return '&';
  204. }
  205. return entity;
  206. }
  207. // Read a JSX identifier (valid tag or attribute name).
  208. //
  209. // Optimized version since JSX identifiers can't contain
  210. // escape characters and so can be read as single slice.
  211. // Also assumes that first character was already checked
  212. // by isIdentifierStart in readToken.
  213. jsx_readWord() {
  214. let ch, start = this.pos;
  215. do {
  216. ch = this.input.charCodeAt(++this.pos);
  217. } while (isIdentifierChar(ch) || ch === 45); // '-'
  218. return this.finishToken(tok.jsxName, this.input.slice(start, this.pos));
  219. }
  220. // Parse next token as JSX identifier
  221. jsx_parseIdentifier() {
  222. let node = this.startNode();
  223. if (this.type === tok.jsxName)
  224. node.name = this.value;
  225. else if (this.type.keyword)
  226. node.name = this.type.keyword;
  227. else
  228. this.unexpected();
  229. this.next();
  230. return this.finishNode(node, 'JSXIdentifier');
  231. }
  232. // Parse namespaced identifier.
  233. jsx_parseNamespacedName() {
  234. let startPos = this.start, startLoc = this.startLoc;
  235. let name = this.jsx_parseIdentifier();
  236. if (!options.allowNamespaces || !this.eat(tt.colon)) return name;
  237. var node = this.startNodeAt(startPos, startLoc);
  238. node.namespace = name;
  239. node.name = this.jsx_parseIdentifier();
  240. return this.finishNode(node, 'JSXNamespacedName');
  241. }
  242. // Parses element name in any form - namespaced, member
  243. // or single identifier.
  244. jsx_parseElementName() {
  245. if (this.type === tok.jsxTagEnd) return '';
  246. let startPos = this.start, startLoc = this.startLoc;
  247. let node = this.jsx_parseNamespacedName();
  248. if (this.type === tt.dot && node.type === 'JSXNamespacedName' && !options.allowNamespacedObjects) {
  249. this.unexpected();
  250. }
  251. while (this.eat(tt.dot)) {
  252. let newNode = this.startNodeAt(startPos, startLoc);
  253. newNode.object = node;
  254. newNode.property = this.jsx_parseIdentifier();
  255. node = this.finishNode(newNode, 'JSXMemberExpression');
  256. }
  257. return node;
  258. }
  259. // Parses any type of JSX attribute value.
  260. jsx_parseAttributeValue() {
  261. switch (this.type) {
  262. case tt.braceL:
  263. let node = this.jsx_parseExpressionContainer();
  264. if (node.expression.type === 'JSXEmptyExpression')
  265. this.raise(node.start, 'JSX attributes must only be assigned a non-empty expression');
  266. return node;
  267. case tok.jsxTagStart:
  268. case tt.string:
  269. return this.parseExprAtom();
  270. default:
  271. this.raise(this.start, 'JSX value should be either an expression or a quoted JSX text');
  272. }
  273. }
  274. // JSXEmptyExpression is unique type since it doesn't actually parse anything,
  275. // and so it should start at the end of last read token (left brace) and finish
  276. // at the beginning of the next one (right brace).
  277. jsx_parseEmptyExpression() {
  278. let node = this.startNodeAt(this.lastTokEnd, this.lastTokEndLoc);
  279. return this.finishNodeAt(node, 'JSXEmptyExpression', this.start, this.startLoc);
  280. }
  281. // Parses JSX expression enclosed into curly brackets.
  282. jsx_parseExpressionContainer() {
  283. let node = this.startNode();
  284. this.next();
  285. node.expression = this.type === tt.braceR
  286. ? this.jsx_parseEmptyExpression()
  287. : this.parseExpression();
  288. this.expect(tt.braceR);
  289. return this.finishNode(node, 'JSXExpressionContainer');
  290. }
  291. // Parses following JSX attribute name-value pair.
  292. jsx_parseAttribute() {
  293. let node = this.startNode();
  294. if (this.eat(tt.braceL)) {
  295. this.expect(tt.ellipsis);
  296. node.argument = this.parseMaybeAssign();
  297. this.expect(tt.braceR);
  298. return this.finishNode(node, 'JSXSpreadAttribute');
  299. }
  300. node.name = this.jsx_parseNamespacedName();
  301. node.value = this.eat(tt.eq) ? this.jsx_parseAttributeValue() : null;
  302. return this.finishNode(node, 'JSXAttribute');
  303. }
  304. // Parses JSX opening tag starting after '<'.
  305. jsx_parseOpeningElementAt(startPos, startLoc) {
  306. let node = this.startNodeAt(startPos, startLoc);
  307. node.attributes = [];
  308. let nodeName = this.jsx_parseElementName();
  309. if (nodeName) node.name = nodeName;
  310. while (this.type !== tt.slash && this.type !== tok.jsxTagEnd)
  311. node.attributes.push(this.jsx_parseAttribute());
  312. node.selfClosing = this.eat(tt.slash);
  313. this.expect(tok.jsxTagEnd);
  314. return this.finishNode(node, nodeName ? 'JSXOpeningElement' : 'JSXOpeningFragment');
  315. }
  316. // Parses JSX closing tag starting after '</'.
  317. jsx_parseClosingElementAt(startPos, startLoc) {
  318. let node = this.startNodeAt(startPos, startLoc);
  319. let nodeName = this.jsx_parseElementName();
  320. if (nodeName) node.name = nodeName;
  321. this.expect(tok.jsxTagEnd);
  322. return this.finishNode(node, nodeName ? 'JSXClosingElement' : 'JSXClosingFragment');
  323. }
  324. // Parses entire JSX element, including it's opening tag
  325. // (starting after '<'), attributes, contents and closing tag.
  326. jsx_parseElementAt(startPos, startLoc) {
  327. let node = this.startNodeAt(startPos, startLoc);
  328. let children = [];
  329. let openingElement = this.jsx_parseOpeningElementAt(startPos, startLoc);
  330. let closingElement = null;
  331. if (!openingElement.selfClosing) {
  332. contents: for (;;) {
  333. switch (this.type) {
  334. case tok.jsxTagStart:
  335. startPos = this.start; startLoc = this.startLoc;
  336. this.next();
  337. if (this.eat(tt.slash)) {
  338. closingElement = this.jsx_parseClosingElementAt(startPos, startLoc);
  339. break contents;
  340. }
  341. children.push(this.jsx_parseElementAt(startPos, startLoc));
  342. break;
  343. case tok.jsxText:
  344. children.push(this.parseExprAtom());
  345. break;
  346. case tt.braceL:
  347. children.push(this.jsx_parseExpressionContainer());
  348. break;
  349. default:
  350. this.unexpected();
  351. }
  352. }
  353. if (getQualifiedJSXName(closingElement.name) !== getQualifiedJSXName(openingElement.name)) {
  354. this.raise(
  355. closingElement.start,
  356. 'Expected corresponding JSX closing tag for <' + getQualifiedJSXName(openingElement.name) + '>');
  357. }
  358. }
  359. let fragmentOrElement = openingElement.name ? 'Element' : 'Fragment';
  360. node['opening' + fragmentOrElement] = openingElement;
  361. node['closing' + fragmentOrElement] = closingElement;
  362. node.children = children;
  363. if (this.type === tt.relational && this.value === "<") {
  364. this.raise(this.start, "Adjacent JSX elements must be wrapped in an enclosing tag");
  365. }
  366. return this.finishNode(node, 'JSX' + fragmentOrElement);
  367. }
  368. // Parse JSX text
  369. jsx_parseText() {
  370. let node = this.parseLiteral(this.value);
  371. node.type = "JSXText";
  372. return node;
  373. }
  374. // Parses entire JSX element from current position.
  375. jsx_parseElement() {
  376. let startPos = this.start, startLoc = this.startLoc;
  377. this.next();
  378. return this.jsx_parseElementAt(startPos, startLoc);
  379. }
  380. parseExprAtom(refShortHandDefaultPos) {
  381. if (this.type === tok.jsxText)
  382. return this.jsx_parseText();
  383. else if (this.type === tok.jsxTagStart)
  384. return this.jsx_parseElement();
  385. else
  386. return super.parseExprAtom(refShortHandDefaultPos);
  387. }
  388. readToken(code) {
  389. let context = this.curContext();
  390. if (context === tc_expr) return this.jsx_readToken();
  391. if (context === tc_oTag || context === tc_cTag) {
  392. if (isIdentifierStart(code)) return this.jsx_readWord();
  393. if (code == 62) {
  394. ++this.pos;
  395. return this.finishToken(tok.jsxTagEnd);
  396. }
  397. if ((code === 34 || code === 39) && context == tc_oTag)
  398. return this.jsx_readString(code);
  399. }
  400. if (code === 60 && this.exprAllowed && this.input.charCodeAt(this.pos + 1) !== 33) {
  401. ++this.pos;
  402. return this.finishToken(tok.jsxTagStart);
  403. }
  404. return super.readToken(code);
  405. }
  406. updateContext(prevType) {
  407. if (this.type == tt.braceL) {
  408. var curContext = this.curContext();
  409. if (curContext == tc_oTag) this.context.push(tokContexts.b_expr);
  410. else if (curContext == tc_expr) this.context.push(tokContexts.b_tmpl);
  411. else super.updateContext(prevType);
  412. this.exprAllowed = true;
  413. } else if (this.type === tt.slash && prevType === tok.jsxTagStart) {
  414. this.context.length -= 2; // do not consider JSX expr -> JSX open tag -> ... anymore
  415. this.context.push(tc_cTag); // reconsider as closing tag context
  416. this.exprAllowed = false;
  417. } else {
  418. return super.updateContext(prevType);
  419. }
  420. }
  421. };
  422. }