inject.js 13 KB

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