Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions ExtendedExpressions/Help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ The following operators are supported, in decreasing precedence order:
x <= y Comparison (less than or equal)

x = y Equality
x == y Equality (synonym for =)
x != y Inequality

x & y Bitwise AND
Expand All @@ -67,6 +68,16 @@ The following operators are supported, in decreasing precedence order:
x ? y : z Conditional evaluation


Functions:

abs(x) Absolute value
ceil(x) Round up to integer value
floor(x) Round down to integer value
round(x) Round to nearest integer value
max(x, y, ...) Greatest of x, y, ...
min(x, y, ...) Least of x, y, ...


Additional Syntax:

"string" String literal. Expressions which evaluate to strings will be
Expand Down Expand Up @@ -94,3 +105,7 @@ Examples:
!extend `1d6[foo]+1d6[bar]+${(1d2-1 ? "foo" : "bar")}`
Internally rolls 1d2-1, then submits a command like "[[3+1d6[bar]+3]]" or
"[[1d6[foo]+2+2]]".

!extend `max(1d2[foo], 1d3[bar], 1d4[baz])`: `${foo}`, `${bar}`, `${baz}`
Internally rolls three dice of different sizes, then displays the largest of
the three rolls along with the values of all three.
121 changes: 114 additions & 7 deletions ExtendedExpressions/extend.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
var ExExp = ExExp || {
parseExpression: function(s, until){
var untilCb = (typeof(until) == typeof(function(){}) ? until : function(tok){ return (tok == until); });

// constants
var ARG_COUNTS = {
'abs': 1,
'ceil': 1,
'floor': 1,
'round': 1,
'max': [1],
'min': [1]
};
var BINARY_PRECEDENCE = {
'?': 1, ':': 2,
'||': 3, '&&': 4,
'|': 5, '^': 6, '&': 7,
'=': 8, '!=': 8,
'=': 8, '==': 8, '!=': 8,
'>=': 9, '>': 9, '<': 9, '<=': 9,
'<<': 10, '>>': 10,
'+': 11, '-': 11,
Expand All @@ -31,6 +41,8 @@ var ExExp = ExExp || {

m = s.match(/^\s+/);
if (m){ return retVal("whitespace", m); }
m = s.match(/^(abs|ceil|floor|round|max|min)[(]/);
if (m){ return retVal("function", m); }
m = s.match(/^[({]/);
if (m){ return retVal("opengroup", m); }
m = s.match(/^[)}]/);
Expand All @@ -39,7 +51,7 @@ var ExExp = ExExp || {
if (m){ return retVal("number", m); }
m = s.match(/^['"]/);
if (m){ return retVal("quote", m); }
m = s.match(/^((\|\|)|(&&)|(!=)|(>=)|(<=)|(<<)|(>>)|(\*\*)|[?:|^&=><%!~])/);
m = s.match(/^((\|\|)|(&&)|(==)|(!=)|(>=)|(<=)|(<<)|(>>)|(\*\*)|[?:|^&=><%!~])/);
if (m){ return retVal("extoperator", m); }
m = s.match(/^[-+*/td]/);
if (m){ return retVal("baseoperator", m); }
Expand Down Expand Up @@ -119,6 +131,10 @@ var ExExp = ExExp || {
operators.push(op);
}

function argListUntil(tok){
return (tok == ',') || (tok == ')');
}

function parseHelper(){
var err;

Expand All @@ -129,6 +145,40 @@ var ExExp = ExExp || {
if (!s.tok){ return "Error: Unrecognized token: " + s.s.split(" ", 1)[0]; }
}
switch (s.tok.type){
case "function":
var func = s.tok.match[1];
var argCounts = ARG_COUNTS[func], minArgs, maxArgs;
if (typeof(argCounts) == typeof(0)){
minArgs = argCounts;
maxArgs = argCounts;
}
else if (typeof(argCounts) == typeof([])){
minArgs = argCounts[0];
maxArgs = argCounts[1];
}
else{
return "Error: Unrecognized function: " + func;
}
var args = [];
while ((s.tok) && (s.tok.text != ')')){
var argTree = ExExp.parseExpression(s, argListUntil);
if (typeof(argTree) == typeof("")){ return argTree; } // error
args.push(argTree);
if (!s.tok){ return "Error: Unterminated function: " + func; }
if (!argListUntil(s.tok.text)){
return "Error: Expected ',' or ')' to continue/close '" + func + "(', but got '" + s.tok.text + "'";
}
}
if ((typeof(minArgs) != typeof(0)) || (minArgs < 0)){ minArgs = args.length; }
if ((typeof(maxArgs) != typeof(0)) || (maxArgs < 0)){ maxArgs = args.length; }
if (args.length < minArgs){
return "Error: Function '" + func + "' requires at least " + minArgs + " argument(s)";
}
if (args.length > maxArgs){
return "Error: Function '" + func + "' requires at most " + maxArgs + " argument(s)";
}
operands.push({'type': "function", 'datatype': "number", 'function': func, 'args': args});
return;
case "number":
operands.push({'type': "number", 'datatype': "number", 'value': parseFloat(s.tok.text)});
return;
Expand All @@ -144,13 +194,16 @@ var ExExp = ExExp || {
return;
case "opengroup":
var opener = s.tok.text, closer = CLOSERS[opener];
operands.push(ExExp.parseExpression(s, closer));
var operand = ExExp.parseExpression(s, closer);
if (typeof(operand) == typeof("")){ return operand; } // error
operands.push(operand);
if (s.tok.text != closer){
return "Error: Expected '" + closer + "' to close '" + opener + "', but got '" + s.tok.text + "'";
}
return;
case "openvariable":
var varExp = ExExp.parseExpression(s, "}");
if (typeof(varExp) == typeof("")){ return varExp; } // error
if (s.tok.text != "}"){
return "Error: Expected '}' to close '${', but got '" + s.tok.text + "'";
}
Expand All @@ -175,7 +228,7 @@ var ExExp = ExExp || {
// push operators and operands to their respective stacks, building sub-ASTs in the operand stack as needed
var err = parseHelper();
if (err){ return err; }
for (popToken(s); (s.tok) && (s.tok.text != until) && ((until) || (s.tok.type != "raw")); popToken(s)){
for (popToken(s); (s.tok) && (!untilCb(s.tok.text)) && ((until) || (s.tok.type != "raw")); popToken(s)){
switch(s.tok.type){
case "extoperator":
case "baseoperator":
Expand All @@ -197,6 +250,7 @@ var ExExp = ExExp || {
else{
s.s = s.s.substring(1);
tableExp = ExExp.parseExpression(s, "]");
if (typeof(tableExp) == typeof("")){ return tableExp; } // error
if (s.tok.text != "]"){
return "Error: Expected ']' to close 't[', but got '" + s.tok.text + "'";
}
Expand Down Expand Up @@ -247,13 +301,22 @@ var ExExp = ExExp || {

sendCommand: function(chunks, asts, evalResults, inline, from, labels){
// constants
var FUNCTION_FUNCTIONS = {
'abs': Math.abs,
'ceil': Math.ceil,
'floor': Math.floor,
'round': Math.round,
'max': Math.max,
'min': Math.min
};
var BINARY_FUNCTIONS = {
'||': function(x, y){ return x || y; },
'&&': function(x, y){ return x && y; },
'|': function(x, y){ return x | y; },
'^': function(x, y){ return x ^ y; },
'&': function(x, y){ return x & y; },
'=': function(x, y){ return x == y; },
'==': function(x, y){ return x == y; },
'!=': function(x, y){ return x != y; },
'>=': function(x, y){ return x >= y; },
'>': function(x, y){ return x > y; },
Expand Down Expand Up @@ -317,6 +380,33 @@ var ExExp = ExExp || {
// if we got here, t.value is the name of a rollable table
t.baseValid = true;
return t;
case "function":
var args = [];
for (var i = 0; i < t.args.length; i++){
x = lazyEval(t.args[i], labels, references, unevalRefs, evalReqs, true);
if (typeof(x) == typeof("")){ return x; } // error
if (x.type == "string"){
x.value = parseFloat(x.value);
x.type = "number";
}
if (x.type != "number"){
// unable to fully evaluate argument
if (t.baseValid){ t.baseValid = false; }
return t;
}
args.push(x.value);
}
// successfully evaluated all arguments
t.type = "number";
t.datatype = "number";
t.value = FUNCTION_FUNCTIONS[t.function].apply(args, args);
for (var i = 0; i < t.args.length; i++){
if (t.args[i].label){ labels[t.args[i].label] = t.args[i]; }
}
delete t.function;
delete t.args;
t.baseValid = true;
return t;
case "unop":
case "unopex":
force = force || (t.type != "unop");
Expand Down Expand Up @@ -450,6 +540,11 @@ var ExExp = ExExp || {

// node has no label; check children
switch(t.type){
case "function":
for (var i = 0; i < t.args.length; i++){
if (hasUnevaluatedLabels(t.args[i])){ return true; }
}
return false;
case "tablename":
case "variable":
if (typeof(t.value) == typeof("")){ return false; }
Expand All @@ -473,7 +568,7 @@ var ExExp = ExExp || {
switch(t.type){
case "number":
case "rollspec":
retval = t.value;
retval = t.value | 0;
break;
case "tablename":
retval = "[" + t.value + "]";
Expand Down Expand Up @@ -655,15 +750,27 @@ var ExExp = ExExp || {
if (tokens[0] == "!exroll"){
inline = false;
chunks.push("/roll ");
asts.push(ExExp.parseExpression(state, null));
var ast = ExExp.parseExpression(state, null);
if (typeof(ast) == typeof("")){
// error
ExExp.write(ast, msg.who, "", "ExExp");
return;
}
asts.push(ast);
state.s = " " + ((state.tok || {'text': ""}).text || "") + state.s;
}
else{
inline = true;
for (var i = state.s.indexOf('`'); i >= 0; i = state.s.indexOf('`')){
chunks.push(state.s.substring(0, i));
state.s = state.s.substring(i + 1);
asts.push(ExExp.parseExpression(state, '`'));
var ast = ExExp.parseExpression(state, '`');
if (typeof(ast) == typeof("")){
// error
ExExp.write(ast, msg.who, "", "ExExp");
return;
}
asts.push(ast);
}
}
chunks.push(state.s);
Expand Down
2 changes: 1 addition & 1 deletion ExtendedExpressions/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ExtendedExpressions",
"version": "0.3",
"version": "0.4",
"description": "Extended roll expression syntax, supporting conditionals, variable references, bitwise operators, and more.",
"authors": "manveti",
"roll20userid": "503018",
Expand Down