UNPKG

ajs

Version:

Asynchronous templating in Node.js

645 lines (570 loc) 19.7 kB
'use strict'; // Thanks to mishoo/uglifyjs for most of this! // [« Back to Index](index.html) var util = require('util'), g = require('./grammar'), Token = require('./lexer').Token; // AJS Parser // ------------- // The parser takes raw token output from the lexer and construct // an AST (abstract syntax tree) according to Javascript and AJS syntax rules. var Parser = module.exports = function Parser(lexer, opts) { opts = opts || {}; this.lexer = lexer; this.tree = []; this.exigentMode = opts.exigentMode == true; this.embedTokens = opts.embedTokens == true; this._token = null; this._peeked = null; this._prevToken = null; this._inFunction = 0; this._inLoop = 0; this._labels = []; }; // We use a recursive look-ahead algorithm to build up the AST. Parse a statement // at a time until we reach the end of the file. Parser.prototype.parse = function () { var self = this; this._pos = 0; var self = this; this.tree = function () { self._next(); var statements = []; while (!self._tokenIs(Token.EOF)) { statements.push(self._statement()); }return self._node(Node.ROOT, statements); }(); return this.tree; }; Parser.prototype._peek = function () { return this._peeked || (this._peeked = this.lexer.nextToken()); }; Parser.prototype._next = function () { this._prevToken = this._token; if (this._peeked) { this._token = this._peeked; this._peeked = null; } else { this._token = this.lexer.nextToken(); } if (!this._token) throw new Error('unexpected eof, after:' + this._prev() + this._prev().line); return this._token; }; Parser.prototype._prev = function () { return this._prevToken; }; Parser.prototype._tokenIs = function (type, value) { return tokenIs(this._token, type, value); }; Parser.prototype._tokenIsPunc = function (value) { return tokenIs(this._token, Token.PUNCTUATION, value); }; Parser.prototype._error = function (msg, line, col) { throw new Error(msg + " at line " + (line || this.lexer._line) + ", column " + (col || this.lexer._col)); }; Parser.prototype._tokenError = function (token, msg) { this._error(msg, token.line, token.col); }; Parser.prototype._unexpected = function (token) { if (token == null) token = this._token; this._tokenError(token, "Unexpected token: " + token.type + " (" + token.value + ")"); }; Parser.prototype._expectToken = function (type, val) { if (this._tokenIs(type, val)) return this._next(); this._tokenError(this._token, "Unexpected token " + this._token.type + ", expected " + type); }; Parser.prototype._expect = function (punc) { return this._expectToken(Token.PUNCTUATION, punc); }; Parser.prototype._canInsertSemicolon = function () { return !this.exigentMode && (this._token.newLineBefore || this._tokenIs(Token.EOF) || this._tokenIsPunc("}")); }; Parser.prototype._semicolon = function () { if (this._tokenIsPunc(";")) this._next();else if (!this._canInsertSemicolon() && !this._tokenIs(Token.OUTPUT)) this._unexpected(); }; Parser.prototype._parenthesised = function () { this._expect("("); var ex = this._expression(); this._expect(")"); return ex; }; Parser.prototype._statement = function () { if (this._tokenIs(Token.OPERATOR, "/")) { this._peeked = null; this._token = this.lexer.nextToken(true); // force regexp } switch (this._token.type) { case Token.OUTPUT: return this._node(Node.OUTPUT, this._prog1(this._token.value, this._next)); case Token.EMBED: return this._embed(); case Token.NUMBER: case Token.STRING: case Token.REGEXP: case Token.OPERATOR: case Token.ATOM: return this._simpleStatement(); case Token.NAME: return tokenIs(this._peek(), Token.PUNCTUATION, ":") ? this._labeledStatement(this._prog1(this._token.value, this._next, this._next)) : this._simpleStatement(); case Token.PUNCTUATION: switch (this._token.value) { case "{": return this._node(Node.BLOCK, this._block()); case "[": case "(": return this._simpleStatement(); case ";": this._next(); return this._node(Node.BLOCK); default: this._unexpected(); } case Token.KEYWORD: switch (this._prog1(this._token.value, this._next)) { case "break": return this._breakCont(Node.BREAK); case "continue": return this._breakCont(Node.CONTINUE); case "debugger": this._semicolon(); return this._node(Node.DEBUGGER); case "do": var self = this; return function (body) { self._expectToken(Token.KEYWORD, "while"); return self._node(Node.DO, self._prog1(self._parenthesised, self._semicolon), body); }(this._loop(this._statement)); case "for": return this._for(); case "function": return this._function(true); case "if": return this._if(); case "return": if (!this._inFunction) this._error("'return' outside of function"); return this._node(Node.RETURN, this._tokenIsPunc(";") ? (this._next(), null) : this._canInsertSemicolon() ? null : this._prog1(this._expression, this._semicolon)); case "switch": return this._node(Node.SWITCH, this._parenthesised(), this._switchBlock()); case "throw": return this._node(Node.THROW, this._prog1(this._expression, this._semicolon)); case "try": return this._try(); case "var": return this._prog1(this._var, this._semicolon); case "const": return this._prog1(this._const, this._semicolon); case "while": return this._node(Node.WHILE, this._parenthesised(), this._loop(this._statement)); case "with": return this._node(Node.WITH, this._parenthesised(), this._statement()); default: this._unexpected(); } break; } }; Parser.prototype._embed = function () { var type = this._token.value; this._next(); switch (type) { case "=": return this._node(Node.EMBED, this._statement()); case "-": return this._node(Node.EMBED_RAW, this._statement()); default: this._unexpected(); } }; Parser.prototype._labeledStatement = function (label) { this._labels.push(label); var start = this._token, statNode = this._statement(); if (this.exigentMode && !g.is_statement_with_label(statNode.type)) this._unexpected(start); this._labels.pop(); return this._node(Node.LABEL, label, statNode); }; Parser.prototype._simpleStatement = function () { return this._node(Node.STATEMENT, this._prog1(this._expression, this._semicolon)); }; Parser.prototype._breakCont = function (type) { var name = this._tokenIs(Token.NAME) ? this._token.value : null; if (name != null) { this._next(); if (!member(name, this._labels)) this._error("Label " + name + " without matching loop or statement"); } else if (!this._inLoop) this._error(type + " not inside a loop or switch"); this._semicolon(); return this._node(type, name); }; Parser.prototype._for = function () { this._expect("("); var init = null; if (!this._tokenIsPunc(";")) { init = this._tokenIs(Token.KEYWORD, "var") ? (this._next(), this._var(true)) : this._expression(true, true); if (this._tokenIs(Token.OPERATOR, "in")) return this._forIn(init); } return this._regularFor(init); }; Parser.prototype._regularFor = function (init) { this._expect(";"); var test = this._tokenIsPunc(";") ? null : this._expression(); this._expect(";"); var step = this._tokenIsPunc(")") ? null : this._expression(); this._expect(")"); return this._node(Node.FOR, init, test, step, this._loop(this._statement)); }; Parser.prototype._forIn = function (init) { var lhs = init.type == Node.VAR ? this._node(Node.NAME, init.children[0][0].children[0]) : init; this._next(); var obj = this._expression(); this._expect(")"); return this._node(Node.FOR_IN, init, lhs, obj, this._loop(this._statement)); }; Parser.prototype._function = function (inStatement) { var name = this._tokenIs(Token.NAME) ? this._prog1(this._token.value, this._next) : null; if (inStatement && !name) this._unexpected(); this._expect("("); var self = this; return this._node(inStatement ? Node.DEFUN : Node.FUNCTION, name, function (first, a) { // arguments while (!self._tokenIsPunc(")")) { if (first) first = false;else self._expect(","); if (!self._tokenIs(Token.NAME)) self._unexpected(); a.push(self._token.value); self._next(); } self._next(); return a; }(true, []), function () { // body ++self._inFunction; var loop = self._inLoop; self._inLoop = 0; var node = self._block(); --self._inFunction; self._inLoop = loop; return node; }()); }; Parser.prototype._if = function () { var cond = this._parenthesised(), body = this._statement(), belse; if (this._tokenIs(Token.KEYWORD, "else")) { this._next(); belse = this._statement(); } return this._node(Node.IF, cond, body, belse); }; Parser.prototype._block = function () { this._expect("{"); var a = []; while (!this._tokenIsPunc("}")) { if (this._tokenIs(Token.EOF)) this._unexpected(); a.push(this._statement()); } this._next(); return a; }; Parser.prototype._switchBlock = function () { var self = this; this._curry(self._loop, function () { self._expect("{"); var node = this._node(Node.BLOCK), cur = null; while (!self._tokenIsPunc("}")) { if (self._tokenIs(Token.EOF)) self._unexpected(); if (self._tokenIs(Token.KEYWORD, "case")) { self._next(); cur = this._node(Node.STATEMENT); node.push([self._expression(), cur]); self._expect(":"); } else if (self._tokenIs(Token.KEYWORD, "default")) { self._next(); self._expect(":"); cur = this._node(Node.STATEMENT); node.push([null, cur]); } else { if (!cur) self._unexpected(); cur.push(self._statement()); } } self._next(); return node; })(); }; Parser.prototype._try = function () { var body = this._block(), bcatch, bfinally; if (this._tokenIs(Token.KEYWORD, "catch")) { this._next(); this._expect("("); if (!this._tokenIs(Token.NAME)) this._error("Name expected"); var name = this._token.value; this._next(); this._expect(")"); bcatch = this._node(Node.BLOCK, name, this._block()); } if (this._tokenIs(Token.KEYWORD, "finally")) { this._next(); bfinally = this._block(); } if (!bcatch && !bfinally) this._error("Missing catch/finally blocks"); return this._node(Node.TRY, body, bcatch, bfinally); }; Parser.prototype._vardefs = function (noIn) { var a = []; for (;;) { if (!this._tokenIs(Token.NAME)) this._unexpected(); var name = this._token.value; this._next(); if (this._tokenIs(Token.OPERATOR, "=")) { this._next(); a.push(this._node(Node.VAR_DEF, name, this._expression(false, noIn))); } else { a.push(this._node(Node.VAR_DEF, name)); } if (!this._tokenIsPunc(",")) break; this._next(); } return a; }; Parser.prototype._var = function (noIn) { return this._node(Node.VAR, this._vardefs(noIn)); }; Parser.prototype._const = function (noIn) { return this._node(Node.CONST, this._vardefs(noIn)); }; Parser.prototype._new = function () { var newexp = this._exprAtom(false), args; if (this._tokenIsPunc("(")) { this._next(); args = this._exprList(")"); } else { args = []; } return this._subscripts(this._node(Node.NEW, newexp, args), true); }; Parser.prototype._exprAtom = function (allowCalls) { if (this._tokenIs(Token.OPERATOR, "new")) { this._next(); return this._new(); } if (this._tokenIs(Token.OPERATOR) && g.is_unary_prefix(this._token.value)) { return this._makeUnary(Node.UNARY_PREFIX, this._prog1(this._token.value, this._next), this._exprAtom(allowCalls)); } if (this._tokenIs(Token.PUNCTUATION)) { switch (this._token.value) { case "(": this._next(); return this._subscripts(this._prog1(this._expression, this._curry(this._expect, ")")), allowCalls); case "[": this._next(); return this._subscripts(this._array(), allowCalls); case "{": this._next(); return this._subscripts(this._object(), allowCalls); } return this._unexpected(); } if (this._tokenIs(Token.KEYWORD, "function")) { this._next(); return this._subscripts(this._function(false), allowCalls); } if (g.is_atomic_start_token(this._token.type)) { var atom = this._tokenIs(Token.REGEXP) ? this._node(Node.REGEXP, this._token.value[0], this._token.value[1]) : this._node(Node[this._token.name], this._token.value); return this._subscripts(this._prog1(atom, this._next), allowCalls); } this._unexpected(); }; Parser.prototype._exprList = function (closing, allowTrailingComma, allowEmpty) { var first = true, a = []; while (!this._tokenIsPunc(closing)) { if (first) first = false;else this._expect(","); if (allowTrailingComma && this._tokenIsPunc(closing)) break; if (this._tokenIsPunc(",") && allowEmpty) { a.push(["atom", "undefined"]); } else { a.push(this._expression(false)); } } this._next(); return a; }; Parser.prototype._array = function () { return this._node(Node.ARRAY, this._exprList("]", !this._exigentMode, true)); }; Parser.prototype._object = function () { var first = true, node = this._node(Node.EXPRESSION_LIST); while (!this._tokenIsPunc("}")) { if (first) first = false;else this._expect(","); if (!this._exigentMode && this._tokenIsPunc("}")) break; // allow trailing comma var type = this._token.type; var name = this._propertyName(); if (type == Token.NAME && (name == "get" || name == "set") && !this._tokenIsPunc(":")) { node.push([this._name(), this._function(false), name]); } else { this._expect(":"); node.push([name, this._expression(false)]); } } this._next(); return this._node(Node.OBJECT, node); }; Parser.prototype._propertyName = function () { switch (this._token.type) { case Token.NUMBER: case Token.STRING: return this._prog1(this._token.value, this._next); } return this._name(); }; Parser.prototype._name = function () { switch (this._token.type) { case Token.NAME: case Token.OPERATOR: case Token.KEYWORD: case Token.ATOM: return this._prog1(this._token.value, this._next); default: this._unexpected(); } }; Parser.prototype._subscripts = function (expr, allowCalls) { if (this._tokenIsPunc(".")) { this._next(); return this._subscripts(this._node(Node.DOT, expr, this._name()), allowCalls); } if (this._tokenIsPunc("[")) { this._next(); return this._subscripts(this._node(Node.SUBSCRIPT, expr, this._prog1(this._expression, this._curry(this._expect, "]"))), allowCalls); } if (allowCalls && this._tokenIsPunc("(")) { this._next(); return this._subscripts(this._node(Node.CALL, expr, this._exprList(")")), true); } if (allowCalls && this._tokenIs(Token.OPERATOR) && g.is_unary_postfix(this._token.value)) { return this._prog1(this._curry(this._makeUnary, Node.UNARY_POSTFIX, this._token.value, expr), this._next); } return expr; }; Parser.prototype._makeUnary = function (name, op, expr) { if ((op == "++" || op == "--") && !this._isAssignable(expr)) this._error("Invalid use of " + op + " operator"); return this._node(name, op, expr); }; Parser.prototype._exprOp = function (left, minPrec, noIn) { var op = this._tokenIs(Token.OPERATOR) ? this._token.value : null; if (op && op == "in" && noIn) op = null; var prec = op != null ? g.precedence(op) : null; if (prec != null && prec > minPrec) { this._next(); var right = this._exprOp(this._exprAtom(true), prec, noIn); return this._exprOp(this._node(Node.BINARY, op, left, right), minPrec, noIn); } return left; }; Parser.prototype._exprOps = function (noIn) { return this._exprOp(this._exprAtom(true), 0, noIn); }; Parser.prototype._maybeTernary = function (noIn) { var expr = this._exprOps(noIn); if (this._tokenIs(Token.OPERATOR, "?")) { this._next(); var yes = this._expression(false); this._expect(":"); return this._node(Node.TERNARY, expr, yes, this._expression(false, noIn)); } return expr; }; Parser.prototype._isAssignable = function (expr) { if (!this.exigentMode) return true; switch (expr.type) { case Node.DOT: case Node.SUBSCRIPT: case Node.NEW: case Node.CALL: return true; case Node.NAME: return expr.value != "this"; } }; Parser.prototype._maybeAssign = function (noIn) { var left = this._maybeTernary(noIn), val = this._token.value; if (this._tokenIs(Token.OPERATOR) && g.is_assignment(val)) { if (this._isAssignable(left)) { this._next(); return this._node(Node.ASSIGN, g.assignment(val), left, this._maybeAssign(noIn)); } this._error("Invalid assignment"); } return left; }; Parser.prototype._expression = function (commas, noIn) { if (arguments.length == 0) commas = true; var expr = this._maybeAssign(noIn); if (commas && this._tokenIsPunc(",")) { this._next(); return this._node(Node.SEQUENCE, expr, this._expression(true, noIn)); } return expr; }; Parser.prototype._loop = function (cont) { try { ++this._inLoop; return cont.call(this); } finally { --this._inLoop; } }; Parser.prototype._curry = function (f) { var args = slice(arguments, 1), self = this; return function () { return f.apply(self, args.concat(slice(arguments))); }; }; Parser.prototype._prog1 = function (ret) { if (ret instanceof Function) ret = ret.call(this); for (var i = 1, n = arguments.length; --n > 0; ++i) { arguments[i].call(this); }return ret; }; Parser.prototype._node = function (type) { var node = new Node(type); node.children = Array.prototype.slice.call(arguments, 1); node.line = this._prevToken && this._prevToken.line || 0; return node; }; var Node = module.exports.Node = function (type) { if (typeof type == 'undefined') throw new Error('undefined node type'); this.type = type; this.line = undefined; this.children = Array.prototype.slice.call(arguments, 1); }; Node.prototype.push = function (child) { this.children.push(child); }; Node.prototype.toString = function () { return '[' + this.type + ', ' + util.inspect(this.children, false, 10) + ']'; }; var nodeTypes = ['arguments', 'array', 'assign', 'atom', 'binary', 'block', 'break', 'call', 'const', 'continue', 'debugger', 'defun', 'do', 'dot', 'embed', 'embed_raw', 'escaped', 'expression_list', 'for', 'for_in', 'function', 'if', 'label', 'name', 'new', 'number', 'object', 'output', 'regexp', 'return', 'root', 'sequence', 'statement', 'string', 'switch', 'subscript', 'ternary', 'throw', 'try', 'unary_postfix', 'unary_prefix', 'var', 'var_def', 'while', 'with']; nodeTypes.forEach(function (name) { name = name.toUpperCase(); Node[name] = 'N_' + name; }); // utilities function tokenIs(token, type, value) { return token.type == type && (value == null || token.value == value); } function slice(a, start) { return Array.prototype.slice.call(a, start == null ? 0 : start); }; function member(name, array) { for (var i = array.length; --i >= 0;) { if (array[i] === name) return true; }return false; };