@airtasker/form-schema-compiler
Version:
a form schema compiler
439 lines (356 loc) • 12.1 kB
JavaScript
;
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;