UNPKG

@airtasker/form-schema-compiler

Version:
439 lines (356 loc) 12.1 kB
"use strict"; exports.__esModule = true; var _flow = require("lodash/flow"); var _flow2 = _interopRequireDefault(_flow); var _const = require("../const"); var _utils = require("./utils"); var utils = _interopRequireWildcard(_utils); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj["default"] = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } /* eslint-disable no-use-before-define */ /** * transpiling token stream to abstract syntax tree * abstract syntax tree https://en.wikipedia.org/wiki/Abstract_syntax_tree * a recursive descent parser https://en.wikipedia.org/wiki/Recursive_descent_parser * There are a lot of ways to write parser, like LL parser LR parser. * recursive descent parser is the easiest way to write. * Because we are building a very simple parser, and need it run in browser, so we chosen using recursive descent way to write. * * e.g. * [{type: operator, value: '-'}, {type: numeric, value: 1}] * to * {type: UnaryExpression, operator: '-', argument: {type: numeric, value: 1}} * @param tokenStream */ var parseExpressionTokenStream = function parseExpressionTokenStream(tokenStream) { var isKeyword = function isKeyword(keyword) { return utils.isKeyword(tokenStream.peek(), keyword); }; var isPunctuation = function isPunctuation(paren) { return utils.isPunctuation(tokenStream.peek(), paren); }; var isOperator = function isOperator(operator) { return utils.isOperator(tokenStream.peek(), operator); }; var isUnary = function isUnary() { return isOperator(_const.OPERATORS.Not) || isOperator(_const.OPERATORS.Add) || isOperator(_const.OPERATORS.Subtract); }; var atomTokenTypes = [_const.TYPES.Identifier, _const.TYPES.Numeric, _const.TYPES.Null, _const.TYPES.String, _const.TYPES.RegExp, _const.TYPES.Boolean]; /** * skip next punctuation, if next char is not punctuation, throw error * @param ch */ var skipPunctuation = function skipPunctuation(ch) { if (!isPunctuation(ch)) { tokenStream.croak("Unexpected token: \"".concat(JSON.stringify(tokenStream.peek()), ", \"Expecting punctuation: \"").concat(ch, "\"")); } tokenStream.next(); }; var skipKeyword = function skipKeyword(keyword) { if (!isKeyword(keyword)) { tokenStream.croak("Unexpected token: \"".concat(JSON.stringify(tokenStream.peek()), ", Expecting keyword: \"").concat(keyword, "\"")); } tokenStream.next(); }; /** * delimited * parse tokens between start, stop, end. * if char is not match start, stop or end, throw an error. * @param start start char, throw exception when not match * @param stop stop char, throw exception when not match * @param separator parser separator throw exception when not match * @param parser parser * @returns {Array} */ var delimited = function delimited(start, stop, separator, parser) { var args = []; var first = true; skipPunctuation(start); while (!tokenStream.eof()) { if (isPunctuation(stop)) { break; } if (first) { first = false; } else { skipPunctuation(separator); } if (isPunctuation(stop)) { break; } args.push(parser()); } skipPunctuation(stop); return args; }; var maybeCallOrMember = (0, _flow2["default"])(maybeCall, maybeMember); /** * return a call expression if next token is '(' * @param callee * @returns {*} */ function maybeCall(callee) { if (isPunctuation(_const.PUNCTUATIONS.Parentheses[0])) { return maybeCallOrMember(parseCall(callee)); } return callee; } /** * return a member expression if next token is '[' * @param object */ function maybeMember(object) { if (isPunctuation(_const.PUNCTUATIONS.SquareBrackets[0])) { return maybeCallOrMember(parseMember(object)); } return object; } /** * return an unary expression if current token is -+! * @param expr * @returns {*} */ function maybeUnary(expr) { var token = isUnary(); if (token) { tokenStream.next(); return { type: _const.TYPES.UnaryExpression, operator: token.value, argument: maybeUnary(expr) }; } return expr(); } /** * return binary expression if next token is an operator * @param left * @param leftOpPrec * @returns {*} */ function maybeBinary(left) { var leftOpPrec = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; var token = isOperator(); if (token) { var rightOpPrec = _const.PRECEDENCE[token.value]; var isAssign = token.value === _const.OPERATORS.Assign; if (rightOpPrec > leftOpPrec) { if (isAssign && left.type !== _const.TYPES.Identifier) { tokenStream.croak("You can only assign to an identifier \"".concat(JSON.stringify(left), "\"")); } tokenStream.next(); var right = maybeBinary(parseAtom(), rightOpPrec); var binary = { type: isAssign ? _const.TYPES.AssignExpression : _const.TYPES.BinaryExpression, operator: token.value, left: left, right: right }; return maybeBinary(binary, leftOpPrec); } } return left; } /** * parse call with arguments * @param callee * @returns {{type: 'CallExpression', callee: callee, arguments: [...]}} */ function parseCall(callee) { return { type: _const.TYPES.CallExpression, callee: callee, arguments: delimited(_const.PUNCTUATIONS.Parentheses[0], // ( _const.PUNCTUATIONS.Parentheses[1], // ) _const.PUNCTUATIONS.Separator, // , parseExpression) }; } /** * parse object * @returns {{type: 'ObjectExpression', properties: [...]}} */ function parseObject() { return { type: _const.TYPES.ObjectExpression, properties: delimited(_const.PUNCTUATIONS.Braces[0], // { _const.PUNCTUATIONS.Braces[1], // } _const.PUNCTUATIONS.Separator, // , parseObjectProperty // should have property key : value ) }; } function parseObjectProperty() { var key = parseAtom(); if (![_const.TYPES.Identifier, _const.TYPES.String, _const.TYPES.Numeric].includes(key.type)) { tokenStream.croak("Object key should only be identifier, string or number, instead of \"".concat(key.value, ":").concat(key.type, "\"")); } skipPunctuation(_const.PUNCTUATIONS.Colon); return { key: key, value: parseExpression() }; } /** * parse array * @returns {{type: 'ArrayExpression', elements: [...]}} */ function parseArray() { return { type: _const.TYPES.ArrayExpression, elements: delimited(_const.PUNCTUATIONS.SquareBrackets[0], // [ _const.PUNCTUATIONS.SquareBrackets[1], // ] _const.PUNCTUATIONS.Separator, // , parseExpression // should have property key : value ) }; } function parseMember(object) { skipPunctuation(_const.PUNCTUATIONS.SquareBrackets[0]); var property = parseExpression(); skipPunctuation(_const.PUNCTUATIONS.SquareBrackets[1]); return { type: _const.TYPES.MemberExpression, object: object, property: property }; } function checkIfNeedAddDummyQuasis(expressions, quasis) { if (quasis.length <= expressions.length) { quasis.push({ type: _const.TYPES.String, value: "" }); } } function parseTemplateLiteral() { skipPunctuation(_const.PUNCTUATIONS.BackQuote); var expressions = []; var quasis = []; while (!isPunctuation(_const.PUNCTUATIONS.BackQuote)) { if (isPunctuation(_const.PUNCTUATIONS.Braces[0])) { skipPunctuation(_const.PUNCTUATIONS.Braces[0]); checkIfNeedAddDummyQuasis(expressions, quasis); expressions.push(parseExpression()); skipPunctuation(_const.PUNCTUATIONS.Braces[1]); } else { quasis.push(tokenStream.next()); } } checkIfNeedAddDummyQuasis(expressions, quasis); skipPunctuation(_const.PUNCTUATIONS.BackQuote); return { type: _const.TYPES.TemplateLiteral, expressions: expressions, quasis: quasis }; } /** * parse next expression * @returns {Expression} */ function parseExpression() { return maybeBinary(parseAtom(), 0); } /** * parse single expression, could be a call expression, an unary expression, an identifier or an expression inside a parentheses * @returns {Expression} */ function parseAtom() { var skipUnaryCheck = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; return maybeUnary(function () { return maybeCallOrMember(parseSimpleAtom()); }); } function parseProgram() { var body = []; while (!tokenStream.eof()) { body.push(parseExpression()); if (tokenStream.eof()) { // skip semi colon check if tokenStream is end; break; } skipPunctuation(_const.PUNCTUATIONS.SemiColon); } return { type: _const.TYPES.Program, body: body }; } function parseBlockStatement() { return { type: _const.TYPES.BlockStatement, body: delimited(_const.PUNCTUATIONS.Braces[0], // { _const.PUNCTUATIONS.Braces[1], // } _const.PUNCTUATIONS.SemiColon, // ; parseExpression) }; } function parseIfStatement() { skipKeyword(_const.IF_KEYWORDS.If); var test = parseExpression(); var consequent = null; var alternate = null; if (isKeyword(_const.IF_KEYWORDS.Then)) { skipKeyword(_const.IF_KEYWORDS.Then); if (isPunctuation(_const.PUNCTUATIONS.Braces[0])) { consequent = parseBlockStatement(); } else { consequent = parseExpression(); } } if (isKeyword(_const.IF_KEYWORDS.Else)) { skipKeyword(_const.IF_KEYWORDS.Else); if (isPunctuation(_const.PUNCTUATIONS.Braces[0])) { alternate = parseBlockStatement(); } else { alternate = parseExpression(); } } return { type: _const.TYPES.IfStatement, test: test, consequent: consequent, alternate: alternate }; } /** * parse a simple atom, e.g identifier, number, string, object, array, boolean, etc * @returns {Expression} */ function parseSimpleAtom() { if (isPunctuation(_const.PUNCTUATIONS.Parentheses[0])) { // if it reads parentheses, then will parse the expression inside the parentheses tokenStream.next(); var exp = parseExpression(); skipPunctuation(_const.PUNCTUATIONS.Parentheses[1]); return exp; } if (isPunctuation(_const.PUNCTUATIONS.Braces[0])) { // if it reads braces start, then it's an Object return parseObject(); } if (isPunctuation(_const.PUNCTUATIONS.SquareBrackets[0])) { // if it reads square brackets start, then it's an Array return parseArray(); } if (isPunctuation(_const.PUNCTUATIONS.BackQuote)) { // if it reads back quote, then it's a Template Literal return parseTemplateLiteral(); } if (isKeyword(_const.IF_KEYWORDS.If)) { return parseIfStatement(); } var token = tokenStream.next(); if (atomTokenTypes.includes(token.type)) { return token; } unexpected(token); } function unexpected() { var token = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : undefined; tokenStream.croak("Unexpected token: ".concat(JSON.stringify(token || tokenStream.peek()))); } return parseProgram(); }; exports["default"] = parseExpressionTokenStream;