Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic Arrow function support + array methods #123

Closed
wants to merge 9 commits into from
132 changes: 107 additions & 25 deletions src/jsep.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
LITERAL = 'Literal',
THIS_EXP = 'ThisExpression',
CALL_EXP = 'CallExpression',
ARROW_EXP = 'ArrowFunctionExpression',
UNARY_EXP = 'UnaryExpression',
BINARY_EXP = 'BinaryExpression',
LOGICAL_EXP = 'LogicalExpression',
Expand All @@ -33,6 +34,8 @@
QUMARK_CODE = 63, // ?
SEMCOL_CODE = 59, // ;
COLON_CODE = 58, // :
EQUAL_CODE = 61, // =
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EQUAL_CODE is unused.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I debated using a Map for the expression_parsers versus an object which has to be keyed by strings? And wasn't sure if the arrow function should be included by default or described in the readme to be added if desired?

GTHAN_CODE = 62, // >

throwError = function(message, index) {
var error = new Error(message + ' at character ' + index);
Expand Down Expand Up @@ -62,6 +65,47 @@
},
// Additional valid identifier chars, apart from a-z, A-Z and 0-9 (except on the starting char)
additional_identifier_chars = {'$': t, '_': t},
// Expression Parsers, keyed by starting character. See ExpressionParser
// { [key: string]: (node) => node }
expression_parsers = {
'?': function ternary(test) {
// Ternary expression: test ? consequent : alternate
var consequent, alternate;
consequent = this.gobbleExpression();
if (!consequent) {
throwError('Expected expression', this.index);
}
this.gobbleSpaces();
if (this.exprICode(this.index) === COLON_CODE) {
this.index++;
alternate = this.gobbleExpression();
if (!alternate) {
throwError('Expected expression', this.index);
}
return {
type: CONDITIONAL_EXP,
test: test,
consequent: consequent,
alternate: alternate
};
} else {
this.throwError('Expected :', this.index);
}
},
'=': function arrow(node) {
// arrow expression: () => expr
if (this.exprICode(this.index) === GTHAN_CODE) {
this.index++;
return {
type: ARROW_EXP,
params: node ? [node] : null,
body: this.gobbleExpression(),
};
} else {
this.throwError('Expected >', this.index);
}
},
},
// Get return the longest key length of any object
getMaxKeyLen = function(obj) {
var max_len = 0, len;
Expand Down Expand Up @@ -139,35 +183,16 @@
}
},

// The main parsing function. Much of this code is dedicated to ternary expressions
// The main parsing function
gobbleExpression = function() {
var test = gobbleBinaryExpression(),
consequent, alternate;
ch;
gobbleSpaces();

if(exprICode(index) === QUMARK_CODE) {
// Ternary expression: test ? consequent : alternate
ch = exprI(index);
if (expression_parsers.hasOwnProperty(ch)) {
index++;
consequent = gobbleExpression();
if(!consequent) {
throwError('Expected expression', index);
}
gobbleSpaces();
if(exprICode(index) === COLON_CODE) {
index++;
alternate = gobbleExpression();
if(!alternate) {
throwError('Expected expression', index);
}
return {
type: CONDITIONAL_EXP,
test: test,
consequent: consequent,
alternate: alternate
};
} else {
throwError('Expected :', index);
}
return expression_parsers[ch].bind(expression_parser_scope)(test, expression_parser_scope);
} else {
return test;
}
Expand Down Expand Up @@ -530,11 +555,22 @@
// then the expression probably doesn't have a `)`
gobbleGroup = function() {
index++;
var node = gobbleExpression();
var node = gobbleExpression(), params;
gobbleSpaces();
if(exprICode(index) === CPAREN_CODE) {
index++;
return node;
} else if (exprICode(index) === COMMA_CODE) {
// arrow function arguments:
index++;
params = gobbleArguments(CPAREN_CODE);
params.unshift(node);
node = gobbleExpression();
if (!node || node.type !== ARROW_EXP) {
throwError('Unclosed (', index);
}
node.params = params;
return node;
} else {
throwError('Unclosed (', index);
}
Expand All @@ -551,6 +587,16 @@
};
},

expression_parser_scope = {
get index() { return index; },
set index(v) { index = v; },
exprI: exprI,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be useful to include all methods here? Too bad it feels like code duplication though...

exprICode: exprICode,
gobbleSpaces: gobbleSpaces,
gobbleExpression: gobbleExpression,
throwError: throwError,
},

nodes = [], ch_i, node;

while(index < length) {
Expand Down Expand Up @@ -618,6 +664,24 @@
additional_identifier_chars[char] = t; return this;
};

/**
* This callback is an expression parser
* @callback ExpressionParser
* @this {{ index: number, exprI: function, exprICode: function, gobbleSpaces: function, gobbleExpression: function, throwError: function}}
* @param {*} node
* @param {*} thisAsArg - same as @this
* @returns {*} node
*/
/**
* @method jsep.addExpressionParser
* @param {string} char The additional character to treat as the start of an expression
* @param {ExpressionParser} fn
* @return jsep
*/
jsep.addExpressionParser = function(char, fn) {
expression_parsers[char] = fn; return this;
};

/**
* @method jsep.addLiteral
* @param {string} literal_name The name of the literal to add
Expand Down Expand Up @@ -663,6 +727,24 @@
return this;
};

/**
* @method jsep.removeExpressionParser
* @param {string} char The character to stop treating as a valid part of an expression
* @return jsep
*/
jsep.removeExpressionParser = function(char) {
delete expression_parsers[char];
return this;
};

/**
* @method jsep.removeAllExpressionParsers
* @return jsep
*/
jsep.removeAllExpressionParsers = function() {
expression_parsers = {};
return this;
};

/**
* @method jsep.removeBinaryOp
Expand Down
33 changes: 33 additions & 0 deletions test/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ QUnit.test('Function Calls', function(assert) {
test_parser("'a'.toString()", {}, assert);
test_parser("[1].length", {}, assert);
test_parser(";", {}, assert);
test_parser("a().b(1)", {}, assert);
test_parser("(['a', 'b'].find(v => v === 'b').length > 1 || 2) === true", {}, assert);
test_parser('a.find((val, key) => key === "abc")', {}, assert);
test_parser('a.find(val => key === "abc")', {}, assert);
test_parser('a.find(() => true)', {}, assert);
test_parser("a.find(() => []).length > 2", {}, assert);
test_parser('[1, 2].find(v => v === 2) >= 0', {}, assert);
test_parser('(a || b).find(v => v(1))', {}, assert);
});

QUnit.test('Arrays', function(assert) {
Expand Down Expand Up @@ -178,6 +186,31 @@ QUnit.test('Custom identifier characters', function(assert) {
}, assert);
});

QUnit.test('Custom identifier characters', function(assert) {
jsep.addExpressionParser('~', function(node){
return {
type: 'CUSTOM',
index: this.index,
val: node.value,
};
});
test_parser("1 ~ 2", {
type: 'Compound',
body: [
{
type: 'CUSTOM',
index: 3,
val: 1,
},
{
type: 'Literal',
value: 2,
raw: '2',
},
],
}, assert);
});

QUnit.test('Bad Numbers', function(assert) {
test_parser("1.", {type: "Literal", value: 1, raw: "1."}, assert);
assert.throws(function(){
Expand Down