"use strict"; var acorn = require(".."); 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: "\"", amp: "&", apos: "'", lt: "<", gt: ">", nbsp: " ", iexcl: "¡", cent: "¢", pound: "£", curren: "¤", yen: "¥", brvbar: "¦", sect: "§", uml: "¨", copy: "©", ordf: "ª", laquo: "«", not: "¬", shy: "­", reg: "®", macr: "¯", deg: "°", plusmn: "±", sup2: "²", sup3: "³", acute: "´", micro: "µ", para: "¶", middot: "·", cedil: "¸", sup1: "¹", ordm: "º", raquo: "»", frac14: "¼", frac12: "½", frac34: "¾", iquest: "¿", Agrave: "À", Aacute: "Á", Acirc: "Â", Atilde: "Ã", Auml: "Ä", Aring: "Å", AElig: "Æ", Ccedil: "Ç", Egrave: "È", Eacute: "É", Ecirc: "Ê", Euml: "Ë", Igrave: "Ì", Iacute: "Í", Icirc: "Î", Iuml: "Ï", ETH: "Ð", Ntilde: "Ñ", Ograve: "Ò", Oacute: "Ó", Ocirc: "Ô", Otilde: "Õ", Ouml: "Ö", times: "×", Oslash: "Ø", Ugrave: "Ù", Uacute: "Ú", Ucirc: "Û", Uuml: "Ü", Yacute: "Ý", THORN: "Þ", szlig: "ß", agrave: "à", aacute: "á", acirc: "â", atilde: "ã", auml: "ä", aring: "å", aelig: "æ", ccedil: "ç", egrave: "è", eacute: "é", ecirc: "ê", euml: "ë", igrave: "ì", iacute: "í", icirc: "î", iuml: "ï", eth: "ð", ntilde: "ñ", ograve: "ò", oacute: "ó", ocirc: "ô", otilde: "õ", ouml: "ö", divide: "÷", oslash: "ø", ugrave: "ù", uacute: "ú", ucirc: "û", uuml: "ü", yacute: "ý", thorn: "þ", yuml: "ÿ", OElig: "Œ", oelig: "œ", Scaron: "Š", scaron: "š", Yuml: "Ÿ", fnof: "ƒ", circ: "ˆ", tilde: "˜", Alpha: "Α", Beta: "Β", Gamma: "Γ", Delta: "Δ", Epsilon: "Ε", Zeta: "Ζ", Eta: "Η", Theta: "Θ", Iota: "Ι", Kappa: "Κ", Lambda: "Λ", Mu: "Μ", Nu: "Ν", Xi: "Ξ", Omicron: "Ο", Pi: "Π", Rho: "Ρ", Sigma: "Σ", Tau: "Τ", Upsilon: "Υ", Phi: "Φ", Chi: "Χ", Psi: "Ψ", Omega: "Ω", alpha: "α", beta: "β", gamma: "γ", delta: "δ", epsilon: "ε", zeta: "ζ", eta: "η", theta: "θ", iota: "ι", kappa: "κ", lambda: "λ", mu: "μ", nu: "ν", xi: "ξ", omicron: "ο", pi: "π", rho: "ρ", sigmaf: "ς", sigma: "σ", tau: "τ", upsilon: "υ", phi: "φ", chi: "χ", psi: "ψ", omega: "ω", thetasym: "ϑ", upsih: "ϒ", piv: "ϖ", ensp: " ", emsp: " ", thinsp: " ", zwnj: "‌", zwj: "‍", lrm: "‎", rlm: "‏", ndash: "–", mdash: "—", lsquo: "‘", rsquo: "’", sbquo: "‚", ldquo: "“", rdquo: "”", bdquo: "„", dagger: "†", Dagger: "‡", bull: "•", hellip: "…", permil: "‰", prime: "′", Prime: "″", lsaquo: "‹", rsaquo: "›", oline: "‾", frasl: "⁄", euro: "€", image: "ℑ", weierp: "℘", real: "ℜ", trade: "™", alefsym: "ℵ", larr: "←", uarr: "↑", rarr: "→", darr: "↓", harr: "↔", crarr: "↵", lArr: "⇐", uArr: "⇑", rArr: "⇒", dArr: "⇓", hArr: "⇔", forall: "∀", part: "∂", exist: "∃", empty: "∅", nabla: "∇", isin: "∈", notin: "∉", ni: "∋", prod: "∏", sum: "∑", minus: "−", lowast: "∗", radic: "√", prop: "∝", infin: "∞", ang: "∠", and: "∧", or: "∨", cap: "∩", cup: "∪", int: "∫", there4: "∴", sim: "∼", cong: "≅", asymp: "≈", ne: "≠", equiv: "≡", le: "≤", ge: "≥", sub: "⊂", sup: "⊃", nsub: "⊄", sube: "⊆", supe: "⊇", oplus: "⊕", otimes: "⊗", perp: "⊥", sdot: "⋅", lceil: "⌈", rceil: "⌉", lfloor: "⌊", rfloor: "⌋", lang: "〈", rang: "〉", loz: "◊", spades: "♠", clubs: "♣", hearts: "♥", diams: "♦" }; 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.markPosition(); 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.markPosition(); 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.markPosition(); 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) { 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); } }; }); };