var acorn = require("../acorn"); var tt = acorn.tokTypes; var tc = acorn.tokContexts; tc.j_oTag = new acorn.TokContext("...", true, true); tt.jsxName = new acorn.TokenType("jsxName"); tt.jsxText = new acorn.TokenType("jsxText", {beforeExpr: true}); tt.jsxTagStart = new acorn.TokenType("jsxTagStart"); tt.jsxTagEnd = new acorn.TokenType("jsxTagEnd"); tt.jsxTagStart.updateContext = function() { this.context.push(tc.j_expr); // treat as beginning of JSX expression this.context.push(tc.j_oTag); // start opening tag context this.exprAllowed = false; }; tt.jsxTagEnd.updateContext = function(prevType) { var out = this.context.pop(); if (out === tc.j_oTag && prevType === tt.slash || out === tc.j_cTag) { this.context.pop(); this.exprAllowed = this.curContext() === tc.j_expr; } else { this.exprAllowed = true; } }; var pp = acorn.Parser.prototype; // Reads inline JSX contents token. pp.jsx_readToken = function() { var out = "", chunkStart = this.pos; for (;;) { if (this.pos >= this.input.length) this.raise(this.start, "Unterminated JSX contents"); var ch = this.input.charCodeAt(this.pos); switch (ch) { case 60: // '<' case 123: // '{' if (this.pos === this.start) { if (ch === 60 && this.exprAllowed) { ++this.pos; return this.finishToken(tt.jsxTagStart); } return this.getTokenFromCode(ch); } out += this.input.slice(chunkStart, this.pos); return this.finishToken(tt.jsxText, out); case 38: // '&' out += this.input.slice(chunkStart, this.pos); out += this.jsx_readEntity(); chunkStart = this.pos; break; default: if (acorn.isNewLine(ch)) { out += this.input.slice(chunkStart, this.pos); ++this.pos; if (ch === 13 && this.input.charCodeAt(this.pos) === 10) { ++this.pos; out += "\n"; } else { out += String.fromCharCode(ch); } if (this.options.locations) { ++this.curLine; this.lineStart = this.pos; } chunkStart = this.pos; } else { ++this.pos; } } } }; pp.jsx_readString = function(quote) { var out = "", chunkStart = ++this.pos; for (;;) { if (this.pos >= this.input.length) this.raise(this.start, "Unterminated string constant"); var ch = this.input.charCodeAt(this.pos); if (ch === quote) break; if (ch === 38) { // '&' out += this.input.slice(chunkStart, this.pos); out += this.jsx_readEntity(); chunkStart = this.pos; } else { ++this.pos; } } out += this.input.slice(chunkStart, this.pos++); return this.finishToken(tt.string, out); }; var XHTMLEntities = { quot: '\u0022', amp: '&', apos: '\u0027', lt: '<', gt: '>', nbsp: '\u00A0', iexcl: '\u00A1', cent: '\u00A2', pound: '\u00A3', curren: '\u00A4', yen: '\u00A5', brvbar: '\u00A6', sect: '\u00A7', uml: '\u00A8', copy: '\u00A9', ordf: '\u00AA', laquo: '\u00AB', not: '\u00AC', shy: '\u00AD', reg: '\u00AE', macr: '\u00AF', deg: '\u00B0', plusmn: '\u00B1', sup2: '\u00B2', sup3: '\u00B3', acute: '\u00B4', micro: '\u00B5', para: '\u00B6', middot: '\u00B7', cedil: '\u00B8', sup1: '\u00B9', ordm: '\u00BA', raquo: '\u00BB', frac14: '\u00BC', frac12: '\u00BD', frac34: '\u00BE', iquest: '\u00BF', Agrave: '\u00C0', Aacute: '\u00C1', Acirc: '\u00C2', Atilde: '\u00C3', Auml: '\u00C4', Aring: '\u00C5', AElig: '\u00C6', Ccedil: '\u00C7', Egrave: '\u00C8', Eacute: '\u00C9', Ecirc: '\u00CA', Euml: '\u00CB', Igrave: '\u00CC', Iacute: '\u00CD', Icirc: '\u00CE', Iuml: '\u00CF', ETH: '\u00D0', Ntilde: '\u00D1', Ograve: '\u00D2', Oacute: '\u00D3', Ocirc: '\u00D4', Otilde: '\u00D5', Ouml: '\u00D6', times: '\u00D7', Oslash: '\u00D8', Ugrave: '\u00D9', Uacute: '\u00DA', Ucirc: '\u00DB', Uuml: '\u00DC', Yacute: '\u00DD', THORN: '\u00DE', szlig: '\u00DF', agrave: '\u00E0', aacute: '\u00E1', acirc: '\u00E2', atilde: '\u00E3', auml: '\u00E4', aring: '\u00E5', aelig: '\u00E6', ccedil: '\u00E7', egrave: '\u00E8', eacute: '\u00E9', ecirc: '\u00EA', euml: '\u00EB', igrave: '\u00EC', iacute: '\u00ED', icirc: '\u00EE', iuml: '\u00EF', eth: '\u00F0', ntilde: '\u00F1', ograve: '\u00F2', oacute: '\u00F3', ocirc: '\u00F4', otilde: '\u00F5', ouml: '\u00F6', divide: '\u00F7', oslash: '\u00F8', ugrave: '\u00F9', uacute: '\u00FA', ucirc: '\u00FB', uuml: '\u00FC', yacute: '\u00FD', thorn: '\u00FE', yuml: '\u00FF', OElig: '\u0152', oelig: '\u0153', Scaron: '\u0160', scaron: '\u0161', Yuml: '\u0178', fnof: '\u0192', circ: '\u02C6', tilde: '\u02DC', Alpha: '\u0391', Beta: '\u0392', Gamma: '\u0393', Delta: '\u0394', Epsilon: '\u0395', Zeta: '\u0396', Eta: '\u0397', Theta: '\u0398', Iota: '\u0399', Kappa: '\u039A', Lambda: '\u039B', Mu: '\u039C', Nu: '\u039D', Xi: '\u039E', Omicron: '\u039F', Pi: '\u03A0', Rho: '\u03A1', Sigma: '\u03A3', Tau: '\u03A4', Upsilon: '\u03A5', Phi: '\u03A6', Chi: '\u03A7', Psi: '\u03A8', Omega: '\u03A9', alpha: '\u03B1', beta: '\u03B2', gamma: '\u03B3', delta: '\u03B4', epsilon: '\u03B5', zeta: '\u03B6', eta: '\u03B7', theta: '\u03B8', iota: '\u03B9', kappa: '\u03BA', lambda: '\u03BB', mu: '\u03BC', nu: '\u03BD', xi: '\u03BE', omicron: '\u03BF', pi: '\u03C0', rho: '\u03C1', sigmaf: '\u03C2', sigma: '\u03C3', tau: '\u03C4', upsilon: '\u03C5', phi: '\u03C6', chi: '\u03C7', psi: '\u03C8', omega: '\u03C9', thetasym: '\u03D1', upsih: '\u03D2', piv: '\u03D6', ensp: '\u2002', emsp: '\u2003', thinsp: '\u2009', zwnj: '\u200C', zwj: '\u200D', lrm: '\u200E', rlm: '\u200F', ndash: '\u2013', mdash: '\u2014', lsquo: '\u2018', rsquo: '\u2019', sbquo: '\u201A', ldquo: '\u201C', rdquo: '\u201D', bdquo: '\u201E', dagger: '\u2020', Dagger: '\u2021', bull: '\u2022', hellip: '\u2026', permil: '\u2030', prime: '\u2032', Prime: '\u2033', lsaquo: '\u2039', rsaquo: '\u203A', oline: '\u203E', frasl: '\u2044', euro: '\u20AC', image: '\u2111', weierp: '\u2118', real: '\u211C', trade: '\u2122', alefsym: '\u2135', larr: '\u2190', uarr: '\u2191', rarr: '\u2192', darr: '\u2193', harr: '\u2194', crarr: '\u21B5', lArr: '\u21D0', uArr: '\u21D1', rArr: '\u21D2', dArr: '\u21D3', hArr: '\u21D4', forall: '\u2200', part: '\u2202', exist: '\u2203', empty: '\u2205', nabla: '\u2207', isin: '\u2208', notin: '\u2209', ni: '\u220B', prod: '\u220F', sum: '\u2211', minus: '\u2212', lowast: '\u2217', radic: '\u221A', prop: '\u221D', infin: '\u221E', ang: '\u2220', and: '\u2227', or: '\u2228', cap: '\u2229', cup: '\u222A', 'int': '\u222B', there4: '\u2234', sim: '\u223C', cong: '\u2245', asymp: '\u2248', ne: '\u2260', equiv: '\u2261', le: '\u2264', ge: '\u2265', sub: '\u2282', sup: '\u2283', nsub: '\u2284', sube: '\u2286', supe: '\u2287', oplus: '\u2295', otimes: '\u2297', perp: '\u22A5', sdot: '\u22C5', lceil: '\u2308', rceil: '\u2309', lfloor: '\u230A', rfloor: '\u230B', lang: '\u2329', rang: '\u232A', loz: '\u25CA', spades: '\u2660', clubs: '\u2663', hearts: '\u2665', diams: '\u2666' }; var hexNumber = /^[\da-fA-F]+$/; var decimalNumber = /^\d+$/; pp.jsx_readEntity = function() { var str = "", count = 0, entity; var ch = this.input[this.pos]; if (ch !== "&") this.raise(this.pos, "Entity must start with an ampersand"); var startPos = ++this.pos; while (this.pos < this.input.length && count++ < 10) { ch = this.input[this.pos++]; if (ch === ";") { if (str[0] === "#") { if (str[1] === "x") { str = str.substr(2); if (hexNumber.test(str)) entity = String.fromCharCode(parseInt(str, 16)); } else { str = str.substr(1); if (decimalNumber.test(str)) entity = String.fromCharCode(parseInt(str, 10)); } } else { entity = XHTMLEntities[str]; } break; } str += ch; } if (!entity) { this.pos = startPos; return "&"; } return entity; }; // Read a JSX identifier (valid tag or attribute name). // // Optimized version since JSX identifiers can't contain // escape characters and so can be read as single slice. // Also assumes that first character was already checked // by isIdentifierStart in readToken. pp.jsx_readWord = function() { var ch, start = this.pos; do { ch = this.input.charCodeAt(++this.pos); } while (acorn.isIdentifierChar(ch) || ch === 45); // '-' return this.finishToken(tt.jsxName, this.input.slice(start, this.pos)); }; // Transforms JSX element name to string. function getQualifiedJSXName(object) { if (object.type === "JSXIdentifier") return object.name; if (object.type === "JSXNamespacedName") return object.namespace.name + ':' + object.name.name; if (object.type === "JSXMemberExpression") return getQualifiedJSXName(object.object) + '.' + getQualifiedJSXName(object.property); } // Parse next token as JSX identifier pp.jsx_parseIdentifier = function() { var node = this.startNode(); if (this.type === tt.jsxName) node.name = this.value; else if (this.type.keyword) node.name = this.type.keyword; else this.unexpected(); this.next(); return this.finishNode(node, "JSXIdentifier"); }; // Parse namespaced identifier. pp.jsx_parseNamespacedName = function() { var start = this.currentPos(); var name = this.jsx_parseIdentifier(); if (!this.eat(tt.colon)) return name; var node = this.startNodeAt(start); node.namespace = name; node.name = this.jsx_parseIdentifier(); return this.finishNode(node, "JSXNamespacedName"); }; // Parses element name in any form - namespaced, member // or single identifier. pp.jsx_parseElementName = function() { var start = this.currentPos(); var node = this.jsx_parseNamespacedName(); while (this.eat(tt.dot)) { var newNode = this.startNodeAt(start); newNode.object = node; newNode.property = this.jsx_parseIdentifier(); node = this.finishNode(newNode, "JSXMemberExpression"); } return node; }; // Parses any type of JSX attribute value. pp.jsx_parseAttributeValue = function() { switch (this.type) { case tt.braceL: var node = this.jsx_parseExpressionContainer(); if (node.expression.type === "JSXEmptyExpression") this.raise(node.start, "JSX attributes must only be assigned a non-empty expression"); return node; case tt.jsxTagStart: case tt.string: return this.parseExprAtom(); default: this.raise(this.start, "JSX value should be either an expression or a quoted JSX text"); } }; // JSXEmptyExpression is unique type since it doesn't actually parse anything, // and so it should start at the end of last read token (left brace) and finish // at the beginning of the next one (right brace). pp.jsx_parseEmptyExpression = function() { var tmp = this.start; this.start = this.lastTokEnd; this.lastTokEnd = tmp; tmp = this.startLoc; this.startLoc = this.lastTokEndLoc; this.lastTokEndLoc = tmp; return this.finishNode(this.startNode(), "JSXEmptyExpression"); }; // Parses JSX expression enclosed into curly brackets. pp.jsx_parseExpressionContainer = function() { var node = this.startNode(); this.next(); node.expression = this.type === tt.braceR ? this.jsx_parseEmptyExpression() : this.parseExpression(); this.expect(tt.braceR); return this.finishNode(node, "JSXExpressionContainer"); }; // Parses following JSX attribute name-value pair. pp.jsx_parseAttribute = function() { var node = this.startNode(); if (this.eat(tt.braceL)) { this.expect(tt.ellipsis); node.argument = this.parseMaybeAssign(); this.expect(tt.braceR); return this.finishNode(node, "JSXSpreadAttribute"); } node.name = this.jsx_parseNamespacedName(); node.value = this.eat(tt.eq) ? this.jsx_parseAttributeValue() : null; return this.finishNode(node, "JSXAttribute"); }; // Parses JSX opening tag starting after '<'. pp.jsx_parseOpeningElementAt = function(start) { var node = this.startNodeAt(start); node.attributes = []; node.name = this.jsx_parseElementName(); while (this.type !== tt.slash && this.type !== tt.jsxTagEnd) node.attributes.push(this.jsx_parseAttribute()); node.selfClosing = this.eat(tt.slash); this.expect(tt.jsxTagEnd); return this.finishNode(node, "JSXOpeningElement"); }; // Parses JSX closing tag starting after '"); } node.openingElement = openingElement; node.closingElement = closingElement; node.children = children; return this.finishNode(node, "JSXElement"); }; // Parses entire JSX element from current position. pp.jsx_parseElement = function() { var start = this.currentPos(); this.next(); return this.jsx_parseElementAt(start); }; acorn.plugins.jsx = function(instance) { instance.extend("parseExprAtom", function(inner) { return function(refShortHandDefaultPos) { if (this.type === tt.jsxText) return this.parseLiteral(this.value); else if (this.type === tt.jsxTagStart) return this.jsx_parseElement(); else return inner.call(this, refShortHandDefaultPos); }; }); instance.extend("readToken", function(inner) { return function(code) { if (!this.inType) { var context = this.curContext(); if (context === tc.j_expr) return this.jsx_readToken(); if (context === tc.j_oTag || context === tc.j_cTag) { if (acorn.isIdentifierStart(code)) return this.jsx_readWord(); if (code == 62) { ++this.pos; return this.finishToken(tt.jsxTagEnd); } if ((code === 34 || code === 39) && context == tc.j_oTag) return this.jsx_readString(code); } if (code === 60 && this.exprAllowed) { ++this.pos; return this.finishToken(tt.jsxTagStart); } } return inner.call(this, code); }; }); instance.extend("updateContext", function(inner) { return function(prevType) { if (this.type == tt.braceL) { var curContext = this.curContext(); if (curContext == tc.j_oTag) this.context.push(tc.b_expr); else if (curContext == tc.j_expr) this.context.push(tc.b_tmpl); else inner.call(this, prevType); this.exprAllowed = true; } else if (this.type === tt.slash && prevType === tt.jsxTagStart) { this.context.length -= 2; // do not consider JSX expr -> JSX open tag -> ... anymore this.context.push(tc.j_cTag); // reconsider as closing tag context this.exprAllowed = false; } else { return inner.call(this, prevType); } }; }); }