Expand Tinymath grammar, including named arguments (#89795)

* Expand Tinymath grammar, including named arguments

* Add tsconfig project

* Fix tests

* Allow named arguments with numeric types

* Remove dashes from named argument validation

* Fix license header

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Wylie Conlon 2021-02-09 10:46:07 -05:00 committed by GitHub
parent 5099eab19f
commit 3cb04fc6d0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 712 additions and 377 deletions

1
.github/CODEOWNERS vendored
View file

@ -26,6 +26,7 @@
/src/plugins/vis_type_xy/ @elastic/kibana-app
/src/plugins/visualize/ @elastic/kibana-app
/src/plugins/visualizations/ @elastic/kibana-app
/packages/kbn-tinymath/ @elastic/kibana-app
# Application Services
/examples/bfetch_explorer/ @elastic/kibana-app-services

View file

@ -4,6 +4,7 @@
"license": "SSPL-1.0 OR Elastic License 2.0",
"private": true,
"main": "src/index.js",
"types": "tinymath.d.ts",
"scripts": {
"kbn:bootstrap": "yarn build",
"build": "../../node_modules/.bin/pegjs -o src/grammar.js src/grammar.pegjs"

View file

@ -156,11 +156,21 @@ function peg$parse(input, options) {
peg$c12 = function(literal) {
return literal;
},
peg$c13 = function(first, rest) { // We can open this up later. Strict for now.
return first + rest.join('');
peg$c13 = function(chars) {
return {
type: 'variable',
value: chars.join(''),
location: simpleLocation(location()),
text: text()
};
},
peg$c14 = function(first, mid) {
return first + mid.map(m => m[0].join('') + m[1].join('')).join('')
peg$c14 = function(rest) {
return {
type: 'variable',
value: rest.join(''),
location: simpleLocation(location()),
text: text()
};
},
peg$c15 = "+",
peg$c16 = peg$literalExpectation("+", false),
@ -168,8 +178,11 @@ function peg$parse(input, options) {
peg$c18 = peg$literalExpectation("-", false),
peg$c19 = function(left, rest) {
return rest.reduce((acc, curr) => ({
type: 'function',
name: curr[0] === '+' ? 'add' : 'subtract',
args: [acc, curr[1]]
args: [acc, curr[1]],
location: simpleLocation(location()),
text: text()
}), left)
},
peg$c20 = "*",
@ -178,8 +191,11 @@ function peg$parse(input, options) {
peg$c23 = peg$literalExpectation("/", false),
peg$c24 = function(left, rest) {
return rest.reduce((acc, curr) => ({
type: 'function',
name: curr[0] === '*' ? 'multiply' : 'divide',
args: [acc, curr[1]]
args: [acc, curr[1]],
location: simpleLocation(location()),
text: text()
}), left)
},
peg$c25 = "(",
@ -196,25 +212,51 @@ function peg$parse(input, options) {
peg$c34 = function(first, rest) {
return [first].concat(rest);
},
peg$c35 = peg$otherExpectation("function"),
peg$c36 = /^[a-z]/,
peg$c37 = peg$classExpectation([["a", "z"]], false, false),
peg$c38 = function(name, args) {
return {name: name.join(''), args: args || []};
peg$c35 = /^["]/,
peg$c36 = peg$classExpectation(["\""], false, false),
peg$c37 = function(value) { return value.join(''); },
peg$c38 = /^[']/,
peg$c39 = peg$classExpectation(["'"], false, false),
peg$c40 = /^[a-zA-Z_]/,
peg$c41 = peg$classExpectation([["a", "z"], ["A", "Z"], "_"], false, false),
peg$c42 = "=",
peg$c43 = peg$literalExpectation("=", false),
peg$c44 = function(name, value) {
return {
type: 'namedArgument',
name: name.join(''),
value: value,
location: simpleLocation(location()),
text: text()
};
},
peg$c45 = peg$otherExpectation("function"),
peg$c46 = /^[a-zA-Z_\-]/,
peg$c47 = peg$classExpectation([["a", "z"], ["A", "Z"], "_", "-"], false, false),
peg$c48 = function(name, args) {
return {
type: 'function',
name: name.join(''),
args: args || [],
location: simpleLocation(location()),
text: text()
};
},
peg$c39 = peg$otherExpectation("number"),
peg$c40 = function() { return parseFloat(text()); },
peg$c41 = /^[eE]/,
peg$c42 = peg$classExpectation(["e", "E"], false, false),
peg$c43 = peg$otherExpectation("exponent"),
peg$c44 = ".",
peg$c45 = peg$literalExpectation(".", false),
peg$c46 = "0",
peg$c47 = peg$literalExpectation("0", false),
peg$c48 = /^[1-9]/,
peg$c49 = peg$classExpectation([["1", "9"]], false, false),
peg$c50 = /^[0-9]/,
peg$c51 = peg$classExpectation([["0", "9"]], false, false),
peg$c49 = peg$otherExpectation("number"),
peg$c50 = function() {
return parseFloat(text());
},
peg$c51 = /^[eE]/,
peg$c52 = peg$classExpectation(["e", "E"], false, false),
peg$c53 = peg$otherExpectation("exponent"),
peg$c54 = ".",
peg$c55 = peg$literalExpectation(".", false),
peg$c56 = "0",
peg$c57 = peg$literalExpectation("0", false),
peg$c58 = /^[1-9]/,
peg$c59 = peg$classExpectation([["1", "9"]], false, false),
peg$c60 = /^[0-9]/,
peg$c61 = peg$classExpectation([["0", "9"]], false, false),
peg$currPos = 0,
peg$savedPos = 0,
@ -456,10 +498,7 @@ function peg$parse(input, options) {
if (s1 !== peg$FAILED) {
s2 = peg$parseNumber();
if (s2 === peg$FAILED) {
s2 = peg$parseVariableWithQuote();
if (s2 === peg$FAILED) {
s2 = peg$parseVariable();
}
s2 = peg$parseVariable();
}
if (s2 !== peg$FAILED) {
s3 = peg$parse_();
@ -489,129 +528,33 @@ function peg$parse(input, options) {
}
function peg$parseVariable() {
var s0, s1, s2, s3, s4;
s0 = peg$currPos;
s1 = peg$parse_();
if (s1 !== peg$FAILED) {
s2 = peg$parseStartChar();
if (s2 !== peg$FAILED) {
s3 = [];
s4 = peg$parseValidChar();
while (s4 !== peg$FAILED) {
s3.push(s4);
s4 = peg$parseValidChar();
}
if (s3 !== peg$FAILED) {
s4 = peg$parse_();
if (s4 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c13(s2, s3);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parseVariableWithQuote() {
var s0, s1, s2, s3, s4, s5, s6, s7, s8;
var s0, s1, s2, s3, s4, s5;
s0 = peg$currPos;
s1 = peg$parse_();
if (s1 !== peg$FAILED) {
s2 = peg$parseQuote();
if (s2 !== peg$FAILED) {
s3 = peg$parseStartChar();
s3 = [];
s4 = peg$parseValidChar();
if (s4 === peg$FAILED) {
s4 = peg$parseSpace();
}
while (s4 !== peg$FAILED) {
s3.push(s4);
s4 = peg$parseValidChar();
if (s4 === peg$FAILED) {
s4 = peg$parseSpace();
}
}
if (s3 !== peg$FAILED) {
s4 = [];
s5 = peg$currPos;
s6 = [];
s7 = peg$parseSpace();
while (s7 !== peg$FAILED) {
s6.push(s7);
s7 = peg$parseSpace();
}
if (s6 !== peg$FAILED) {
s7 = [];
s8 = peg$parseValidChar();
if (s8 !== peg$FAILED) {
while (s8 !== peg$FAILED) {
s7.push(s8);
s8 = peg$parseValidChar();
}
} else {
s7 = peg$FAILED;
}
if (s7 !== peg$FAILED) {
s6 = [s6, s7];
s5 = s6;
} else {
peg$currPos = s5;
s5 = peg$FAILED;
}
} else {
peg$currPos = s5;
s5 = peg$FAILED;
}
while (s5 !== peg$FAILED) {
s4.push(s5);
s5 = peg$currPos;
s6 = [];
s7 = peg$parseSpace();
while (s7 !== peg$FAILED) {
s6.push(s7);
s7 = peg$parseSpace();
}
if (s6 !== peg$FAILED) {
s7 = [];
s8 = peg$parseValidChar();
if (s8 !== peg$FAILED) {
while (s8 !== peg$FAILED) {
s7.push(s8);
s8 = peg$parseValidChar();
}
} else {
s7 = peg$FAILED;
}
if (s7 !== peg$FAILED) {
s6 = [s6, s7];
s5 = s6;
} else {
peg$currPos = s5;
s5 = peg$FAILED;
}
} else {
peg$currPos = s5;
s5 = peg$FAILED;
}
}
s4 = peg$parseQuote();
if (s4 !== peg$FAILED) {
s5 = peg$parseQuote();
s5 = peg$parse_();
if (s5 !== peg$FAILED) {
s6 = peg$parse_();
if (s6 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c14(s3, s4);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
peg$savedPos = s0;
s1 = peg$c13(s3);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@ -632,6 +575,39 @@ function peg$parse(input, options) {
peg$currPos = s0;
s0 = peg$FAILED;
}
if (s0 === peg$FAILED) {
s0 = peg$currPos;
s1 = peg$parse_();
if (s1 !== peg$FAILED) {
s2 = [];
s3 = peg$parseValidChar();
if (s3 !== peg$FAILED) {
while (s3 !== peg$FAILED) {
s2.push(s3);
s3 = peg$parseValidChar();
}
} else {
s2 = peg$FAILED;
}
if (s2 !== peg$FAILED) {
s3 = peg$parse_();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c14(s2);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
}
return s0;
}
@ -911,114 +887,102 @@ function peg$parse(input, options) {
return s0;
}
function peg$parseArguments() {
var s0, s1, s2, s3, s4, s5, s6, s7, s8;
function peg$parseArgument_List() {
var s0, s1, s2, s3, s4, s5, s6, s7;
peg$silentFails++;
s0 = peg$currPos;
s1 = peg$parse_();
s1 = peg$parseArgument();
if (s1 !== peg$FAILED) {
s2 = peg$parseAddSubtract();
if (s2 !== peg$FAILED) {
s3 = [];
s4 = peg$currPos;
s5 = peg$parse_();
s2 = [];
s3 = peg$currPos;
s4 = peg$parse_();
if (s4 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 44) {
s5 = peg$c31;
peg$currPos++;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c32); }
}
if (s5 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 44) {
s6 = peg$c31;
peg$currPos++;
} else {
s6 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c32); }
}
s6 = peg$parse_();
if (s6 !== peg$FAILED) {
s7 = peg$parse_();
s7 = peg$parseArgument();
if (s7 !== peg$FAILED) {
s8 = peg$parseAddSubtract();
if (s8 !== peg$FAILED) {
peg$savedPos = s4;
s5 = peg$c33(s2, s8);
s4 = s5;
} else {
peg$currPos = s4;
s4 = peg$FAILED;
}
peg$savedPos = s3;
s4 = peg$c33(s1, s7);
s3 = s4;
} else {
peg$currPos = s4;
s4 = peg$FAILED;
peg$currPos = s3;
s3 = peg$FAILED;
}
} else {
peg$currPos = s4;
s4 = peg$FAILED;
peg$currPos = s3;
s3 = peg$FAILED;
}
} else {
peg$currPos = s4;
s4 = peg$FAILED;
peg$currPos = s3;
s3 = peg$FAILED;
}
while (s4 !== peg$FAILED) {
s3.push(s4);
s4 = peg$currPos;
s5 = peg$parse_();
} else {
peg$currPos = s3;
s3 = peg$FAILED;
}
while (s3 !== peg$FAILED) {
s2.push(s3);
s3 = peg$currPos;
s4 = peg$parse_();
if (s4 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 44) {
s5 = peg$c31;
peg$currPos++;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c32); }
}
if (s5 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 44) {
s6 = peg$c31;
peg$currPos++;
} else {
s6 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c32); }
}
s6 = peg$parse_();
if (s6 !== peg$FAILED) {
s7 = peg$parse_();
s7 = peg$parseArgument();
if (s7 !== peg$FAILED) {
s8 = peg$parseAddSubtract();
if (s8 !== peg$FAILED) {
peg$savedPos = s4;
s5 = peg$c33(s2, s8);
s4 = s5;
} else {
peg$currPos = s4;
s4 = peg$FAILED;
}
peg$savedPos = s3;
s4 = peg$c33(s1, s7);
s3 = s4;
} else {
peg$currPos = s4;
s4 = peg$FAILED;
peg$currPos = s3;
s3 = peg$FAILED;
}
} else {
peg$currPos = s4;
s4 = peg$FAILED;
peg$currPos = s3;
s3 = peg$FAILED;
}
} else {
peg$currPos = s4;
s4 = peg$FAILED;
peg$currPos = s3;
s3 = peg$FAILED;
}
} else {
peg$currPos = s3;
s3 = peg$FAILED;
}
}
if (s2 !== peg$FAILED) {
s3 = peg$parse_();
if (s3 !== peg$FAILED) {
s4 = peg$parse_();
if (input.charCodeAt(peg$currPos) === 44) {
s4 = peg$c31;
peg$currPos++;
} else {
s4 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c32); }
}
if (s4 === peg$FAILED) {
s4 = null;
}
if (s4 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 44) {
s5 = peg$c31;
peg$currPos++;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c32); }
}
if (s5 === peg$FAILED) {
s5 = null;
}
if (s5 !== peg$FAILED) {
s6 = peg$parse_();
if (s6 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c34(s2, s3);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
peg$savedPos = s0;
s1 = peg$c34(s1, s2);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@ -1044,6 +1008,199 @@ function peg$parse(input, options) {
return s0;
}
function peg$parseString() {
var s0, s1, s2, s3;
s0 = peg$currPos;
if (peg$c35.test(input.charAt(peg$currPos))) {
s1 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c36); }
}
if (s1 !== peg$FAILED) {
s2 = [];
s3 = peg$parseValidChar();
if (s3 !== peg$FAILED) {
while (s3 !== peg$FAILED) {
s2.push(s3);
s3 = peg$parseValidChar();
}
} else {
s2 = peg$FAILED;
}
if (s2 !== peg$FAILED) {
if (peg$c35.test(input.charAt(peg$currPos))) {
s3 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c36); }
}
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c37(s2);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
if (s0 === peg$FAILED) {
s0 = peg$currPos;
if (peg$c38.test(input.charAt(peg$currPos))) {
s1 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c39); }
}
if (s1 !== peg$FAILED) {
s2 = [];
s3 = peg$parseValidChar();
if (s3 !== peg$FAILED) {
while (s3 !== peg$FAILED) {
s2.push(s3);
s3 = peg$parseValidChar();
}
} else {
s2 = peg$FAILED;
}
if (s2 !== peg$FAILED) {
if (peg$c38.test(input.charAt(peg$currPos))) {
s3 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c39); }
}
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c37(s2);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
if (s0 === peg$FAILED) {
s0 = peg$currPos;
s1 = [];
s2 = peg$parseValidChar();
if (s2 !== peg$FAILED) {
while (s2 !== peg$FAILED) {
s1.push(s2);
s2 = peg$parseValidChar();
}
} else {
s1 = peg$FAILED;
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c37(s1);
}
s0 = s1;
}
}
return s0;
}
function peg$parseArgument() {
var s0, s1, s2, s3, s4, s5, s6;
s0 = peg$currPos;
s1 = [];
if (peg$c40.test(input.charAt(peg$currPos))) {
s2 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c41); }
}
if (s2 !== peg$FAILED) {
while (s2 !== peg$FAILED) {
s1.push(s2);
if (peg$c40.test(input.charAt(peg$currPos))) {
s2 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c41); }
}
}
} else {
s1 = peg$FAILED;
}
if (s1 !== peg$FAILED) {
s2 = peg$parse_();
if (s2 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 61) {
s3 = peg$c42;
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c43); }
}
if (s3 !== peg$FAILED) {
s4 = peg$parse_();
if (s4 !== peg$FAILED) {
s5 = peg$parseNumber();
if (s5 === peg$FAILED) {
s5 = peg$parseString();
}
if (s5 !== peg$FAILED) {
s6 = peg$parse_();
if (s6 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c44(s1, s5);
s0 = s1;
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
if (s0 === peg$FAILED) {
s0 = peg$parseAddSubtract();
}
return s0;
}
function peg$parseFunction() {
var s0, s1, s2, s3, s4, s5, s6, s7, s8;
@ -1052,22 +1209,22 @@ function peg$parse(input, options) {
s1 = peg$parse_();
if (s1 !== peg$FAILED) {
s2 = [];
if (peg$c36.test(input.charAt(peg$currPos))) {
if (peg$c46.test(input.charAt(peg$currPos))) {
s3 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c37); }
if (peg$silentFails === 0) { peg$fail(peg$c47); }
}
if (s3 !== peg$FAILED) {
while (s3 !== peg$FAILED) {
s2.push(s3);
if (peg$c36.test(input.charAt(peg$currPos))) {
if (peg$c46.test(input.charAt(peg$currPos))) {
s3 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c37); }
if (peg$silentFails === 0) { peg$fail(peg$c47); }
}
}
} else {
@ -1084,7 +1241,7 @@ function peg$parse(input, options) {
if (s3 !== peg$FAILED) {
s4 = peg$parse_();
if (s4 !== peg$FAILED) {
s5 = peg$parseArguments();
s5 = peg$parseArgument_List();
if (s5 === peg$FAILED) {
s5 = null;
}
@ -1102,7 +1259,7 @@ function peg$parse(input, options) {
s8 = peg$parse_();
if (s8 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c38(s2, s5);
s1 = peg$c48(s2, s5);
s0 = s1;
} else {
peg$currPos = s0;
@ -1139,7 +1296,7 @@ function peg$parse(input, options) {
peg$silentFails--;
if (s0 === peg$FAILED) {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c35); }
if (peg$silentFails === 0) { peg$fail(peg$c45); }
}
return s0;
@ -1174,7 +1331,7 @@ function peg$parse(input, options) {
}
if (s4 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$c40();
s1 = peg$c50();
s0 = s1;
} else {
peg$currPos = s0;
@ -1195,7 +1352,7 @@ function peg$parse(input, options) {
peg$silentFails--;
if (s0 === peg$FAILED) {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c39); }
if (peg$silentFails === 0) { peg$fail(peg$c49); }
}
return s0;
@ -1204,12 +1361,12 @@ function peg$parse(input, options) {
function peg$parseE() {
var s0;
if (peg$c41.test(input.charAt(peg$currPos))) {
if (peg$c51.test(input.charAt(peg$currPos))) {
s0 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s0 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c42); }
if (peg$silentFails === 0) { peg$fail(peg$c52); }
}
return s0;
@ -1261,7 +1418,7 @@ function peg$parse(input, options) {
peg$silentFails--;
if (s0 === peg$FAILED) {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c43); }
if (peg$silentFails === 0) { peg$fail(peg$c53); }
}
return s0;
@ -1272,11 +1429,11 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 46) {
s1 = peg$c44;
s1 = peg$c54;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c45); }
if (peg$silentFails === 0) { peg$fail(peg$c55); }
}
if (s1 !== peg$FAILED) {
s2 = [];
@ -1308,20 +1465,20 @@ function peg$parse(input, options) {
var s0, s1, s2, s3;
if (input.charCodeAt(peg$currPos) === 48) {
s0 = peg$c46;
s0 = peg$c56;
peg$currPos++;
} else {
s0 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c47); }
if (peg$silentFails === 0) { peg$fail(peg$c57); }
}
if (s0 === peg$FAILED) {
s0 = peg$currPos;
if (peg$c48.test(input.charAt(peg$currPos))) {
if (peg$c58.test(input.charAt(peg$currPos))) {
s1 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c49); }
if (peg$silentFails === 0) { peg$fail(peg$c59); }
}
if (s1 !== peg$FAILED) {
s2 = [];
@ -1349,17 +1506,30 @@ function peg$parse(input, options) {
function peg$parseDigit() {
var s0;
if (peg$c50.test(input.charAt(peg$currPos))) {
if (peg$c60.test(input.charAt(peg$currPos))) {
s0 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s0 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c51); }
if (peg$silentFails === 0) { peg$fail(peg$c61); }
}
return s0;
}
function simpleLocation (location) {
// Returns an object representing the position of the function within the expression,
// demarcated by the position of its first character and last character. We calculate these values
// using the offset because the expression could span multiple lines, and we don't want to deal
// with column and line values.
return {
min: location.start.offset,
max: location.end.offset
}
}
peg$result = peg$startRuleFunction();
if (peg$result !== peg$FAILED && peg$currPos === input.length) {

View file

@ -1,5 +1,18 @@
// tinymath parsing grammar
{
function simpleLocation (location) {
// Returns an object representing the position of the function within the expression,
// demarcated by the position of its first character and last character. We calculate these values
// using the offset because the expression could span multiple lines, and we don't want to deal
// with column and line values.
return {
min: location.start.offset,
max: location.end.offset
}
}
}
start
= Expression
@ -23,18 +36,28 @@ ValidChar
// literals and variables
Literal "literal"
= _ literal:(Number / VariableWithQuote / Variable) _ {
= _ literal:(Number / Variable) _ {
return literal;
}
// Quoted variables are interpreted as strings
// but unquoted variables are more restrictive
Variable
= _ first:StartChar rest:ValidChar* _ { // We can open this up later. Strict for now.
return first + rest.join('');
= _ Quote chars:(ValidChar / Space)* Quote _ {
return {
type: 'variable',
value: chars.join(''),
location: simpleLocation(location()),
text: text()
};
}
VariableWithQuote
= _ Quote first:StartChar mid:(Space* ValidChar+)* Quote _ {
return first + mid.map(m => m[0].join('') + m[1].join('')).join('')
/ _ rest:ValidChar+ _ {
return {
type: 'variable',
value: rest.join(''),
location: simpleLocation(location()),
text: text()
};
}
// expressions
@ -45,16 +68,22 @@ Expression
AddSubtract
= _ left:MultiplyDivide rest:(('+' / '-') MultiplyDivide)* _ {
return rest.reduce((acc, curr) => ({
type: 'function',
name: curr[0] === '+' ? 'add' : 'subtract',
args: [acc, curr[1]]
args: [acc, curr[1]],
location: simpleLocation(location()),
text: text()
}), left)
}
MultiplyDivide
= _ left:Factor rest:(('*' / '/') Factor)* _ {
return rest.reduce((acc, curr) => ({
type: 'function',
name: curr[0] === '*' ? 'multiply' : 'divide',
args: [acc, curr[1]]
args: [acc, curr[1]],
location: simpleLocation(location()),
text: text()
}), left)
}
@ -68,20 +97,46 @@ Group
return expr
}
Arguments "arguments"
= _ first:Expression rest:(_ ',' _ arg:Expression {return arg})* _ ','? _ {
Argument_List "arguments"
= first:Argument rest:(_ ',' _ arg:Argument {return arg})* _ ','? {
return [first].concat(rest);
}
String
= [\"] value:(ValidChar)+ [\"] { return value.join(''); }
/ [\'] value:(ValidChar)+ [\'] { return value.join(''); }
/ value:(ValidChar)+ { return value.join(''); }
Argument
= name:[a-zA-Z_]+ _ '=' _ value:(Number / String) _ {
return {
type: 'namedArgument',
name: name.join(''),
value: value,
location: simpleLocation(location()),
text: text()
};
}
/ arg:Expression
Function "function"
= _ name:[a-z]+ '(' _ args:Arguments? _ ')' _ {
return {name: name.join(''), args: args || []};
= _ name:[a-zA-Z_-]+ '(' _ args:Argument_List? _ ')' _ {
return {
type: 'function',
name: name.join(''),
args: args || [],
location: simpleLocation(location()),
text: text()
};
}
// Numbers. Lol.
Number "number"
= '-'? Integer Fraction? Exp? { return parseFloat(text()); }
= '-'? Integer Fraction? Exp? {
return parseFloat(text());
}
E
= [eE]

View file

@ -38,17 +38,22 @@ function interpret(node, scope, injectedFunctions) {
return exec(node);
function exec(node) {
const type = getType(node);
if (typeof node === 'number') {
return node;
}
if (type === 'function') return invoke(node);
if (node.type === 'function') return invoke(node);
if (type === 'string') {
const val = getValue(scope, node);
if (typeof val === 'undefined') throw new Error(`Unknown variable: ${node}`);
if (node.type === 'variable') {
const val = getValue(scope, node.value);
if (typeof val === 'undefined') throw new Error(`Unknown variable: ${node.value}`);
return val;
}
return node; // Can only be a number at this point
if (node.type === 'namedArgument') {
// We are ignoring named arguments in the interpreter
throw new Error(`Named arguments are not supported in tinymath itself, at ${node.name}`);
}
}
function invoke(node) {
@ -67,17 +72,6 @@ function getValue(scope, node) {
return typeof val !== 'undefined' ? val : scope[node];
}
function getType(x) {
const type = typeof x;
if (type === 'object') {
const keys = Object.keys(x);
if (keys.length !== 2 || !x.name || !x.args) throw new Error('Invalid AST object');
return 'function';
}
if (type === 'string' || type === 'number') return type;
throw new Error(`Unknown AST property type: ${type}`);
}
function isOperable(args) {
return args.every((arg) => {
if (Array.isArray(arg)) return isOperable(arg);

View file

@ -11,7 +11,19 @@
Need tests for spacing, etc
*/
const { evaluate, parse } = require('..');
import { evaluate, parse } from '..';
function variableEqual(value) {
return expect.objectContaining({ type: 'variable', value });
}
function functionEqual(name, args) {
return expect.objectContaining({ type: 'function', name, args });
}
function namedArgumentEqual(name, value) {
return expect.objectContaining({ type: 'namedArgument', name, value });
}
describe('Parser', () => {
describe('Numbers', () => {
@ -31,96 +43,144 @@ describe('Parser', () => {
describe('Variables', () => {
it('strings', () => {
expect(parse('f')).toEqual('f');
expect(parse('foo')).toEqual('foo');
expect(parse('f')).toEqual(variableEqual('f'));
expect(parse('foo')).toEqual(variableEqual('foo'));
expect(parse('foo1')).toEqual(variableEqual('foo1'));
expect(() => parse('1foo1')).toThrow('but "f" found');
});
it('strings with spaces', () => {
expect(parse(' foo ')).toEqual(variableEqual('foo'));
expect(() => parse(' foo bar ')).toThrow('but "b" found');
});
it('allowed characters', () => {
expect(parse('_foo')).toEqual('_foo');
expect(parse('@foo')).toEqual('@foo');
expect(parse('.foo')).toEqual('.foo');
expect(parse('-foo')).toEqual('-foo');
expect(parse('_foo0')).toEqual('_foo0');
expect(parse('@foo0')).toEqual('@foo0');
expect(parse('.foo0')).toEqual('.foo0');
expect(parse('-foo0')).toEqual('-foo0');
expect(parse('_foo')).toEqual(variableEqual('_foo'));
expect(parse('@foo')).toEqual(variableEqual('@foo'));
expect(parse('.foo')).toEqual(variableEqual('.foo'));
expect(parse('-foo')).toEqual(variableEqual('-foo'));
expect(parse('_foo0')).toEqual(variableEqual('_foo0'));
expect(parse('@foo0')).toEqual(variableEqual('@foo0'));
expect(parse('.foo0')).toEqual(variableEqual('.foo0'));
expect(parse('-foo0')).toEqual(variableEqual('-foo0'));
});
});
describe('quoted variables', () => {
it('strings with double quotes', () => {
expect(parse('"foo"')).toEqual('foo');
expect(parse('"f b"')).toEqual('f b');
expect(parse('"foo bar"')).toEqual('foo bar');
expect(parse('"foo bar fizz buzz"')).toEqual('foo bar fizz buzz');
expect(parse('"foo bar baby"')).toEqual('foo bar baby');
expect(parse('"foo"')).toEqual(variableEqual('foo'));
expect(parse('"f b"')).toEqual(variableEqual('f b'));
expect(parse('"foo bar"')).toEqual(variableEqual('foo bar'));
expect(parse('"foo bar fizz buzz"')).toEqual(variableEqual('foo bar fizz buzz'));
expect(parse('"foo bar baby"')).toEqual(variableEqual('foo bar baby'));
});
it('strings with single quotes', () => {
/* eslint-disable prettier/prettier */
expect(parse("'foo'")).toEqual('foo');
expect(parse("'f b'")).toEqual('f b');
expect(parse("'foo bar'")).toEqual('foo bar');
expect(parse("'foo bar fizz buzz'")).toEqual('foo bar fizz buzz');
expect(parse("'foo bar baby'")).toEqual('foo bar baby');
expect(parse("'foo'")).toEqual(variableEqual('foo'));
expect(parse("'f b'")).toEqual(variableEqual('f b'));
expect(parse("'foo bar'")).toEqual(variableEqual('foo bar'));
expect(parse("'foo bar fizz buzz'")).toEqual(variableEqual('foo bar fizz buzz'));
expect(parse("'foo bar baby'")).toEqual(variableEqual('foo bar baby'));
expect(parse("' foo bar'")).toEqual(variableEqual(" foo bar"));
expect(parse("'foo bar '")).toEqual(variableEqual("foo bar "));
expect(parse("'0foo'")).toEqual(variableEqual("0foo"));
expect(parse("' foo bar'")).toEqual(variableEqual(" foo bar"));
expect(parse("'foo bar '")).toEqual(variableEqual("foo bar "));
expect(parse("'0foo'")).toEqual(variableEqual("0foo"));
/* eslint-enable prettier/prettier */
});
it('allowed characters', () => {
expect(parse('"_foo bar"')).toEqual('_foo bar');
expect(parse('"@foo bar"')).toEqual('@foo bar');
expect(parse('".foo bar"')).toEqual('.foo bar');
expect(parse('"-foo bar"')).toEqual('-foo bar');
expect(parse('"_foo0 bar1"')).toEqual('_foo0 bar1');
expect(parse('"@foo0 bar1"')).toEqual('@foo0 bar1');
expect(parse('".foo0 bar1"')).toEqual('.foo0 bar1');
expect(parse('"-foo0 bar1"')).toEqual('-foo0 bar1');
});
it('invalid characters in double quotes', () => {
const check = (str) => () => parse(str);
expect(check('" foo bar"')).toThrow('but "\\"" found');
expect(check('"foo bar "')).toThrow('but "\\"" found');
expect(check('"0foo"')).toThrow('but "\\"" found');
expect(check('" foo bar"')).toThrow('but "\\"" found');
expect(check('"foo bar "')).toThrow('but "\\"" found');
expect(check('"0foo"')).toThrow('but "\\"" found');
});
it('invalid characters in single quotes', () => {
const check = (str) => () => parse(str);
/* eslint-disable prettier/prettier */
expect(check("' foo bar'")).toThrow('but "\'" found');
expect(check("'foo bar '")).toThrow('but "\'" found');
expect(check("'0foo'")).toThrow('but "\'" found');
expect(check("' foo bar'")).toThrow('but "\'" found');
expect(check("'foo bar '")).toThrow('but "\'" found');
expect(check("'0foo'")).toThrow('but "\'" found');
/* eslint-enable prettier/prettier */
expect(parse('"_foo bar"')).toEqual(variableEqual('_foo bar'));
expect(parse('"@foo bar"')).toEqual(variableEqual('@foo bar'));
expect(parse('".foo bar"')).toEqual(variableEqual('.foo bar'));
expect(parse('"-foo bar"')).toEqual(variableEqual('-foo bar'));
expect(parse('"_foo0 bar1"')).toEqual(variableEqual('_foo0 bar1'));
expect(parse('"@foo0 bar1"')).toEqual(variableEqual('@foo0 bar1'));
expect(parse('".foo0 bar1"')).toEqual(variableEqual('.foo0 bar1'));
expect(parse('"-foo0 bar1"')).toEqual(variableEqual('-foo0 bar1'));
expect(parse('" foo bar"')).toEqual(variableEqual(' foo bar'));
expect(parse('"foo bar "')).toEqual(variableEqual('foo bar '));
expect(parse('"0foo"')).toEqual(variableEqual('0foo'));
expect(parse('" foo bar"')).toEqual(variableEqual(' foo bar'));
expect(parse('"foo bar "')).toEqual(variableEqual('foo bar '));
expect(parse('"0foo"')).toEqual(variableEqual('0foo'));
});
});
describe('Functions', () => {
it('no arguments', () => {
expect(parse('foo()')).toEqual({ name: 'foo', args: [] });
expect(parse('foo()')).toEqual(functionEqual('foo', []));
});
it('arguments', () => {
expect(parse('foo(5,10)')).toEqual({ name: 'foo', args: [5, 10] });
expect(parse('foo(5,10)')).toEqual(functionEqual('foo', [5, 10]));
});
it('arguments with strings', () => {
expect(parse('foo("string with spaces")')).toEqual({
name: 'foo',
args: ['string with spaces'],
});
expect(parse('foo("string with spaces")')).toEqual(
functionEqual('foo', [variableEqual('string with spaces')])
);
/* eslint-disable prettier/prettier */
expect(parse("foo('string with spaces')")).toEqual({
name: 'foo',
args: ['string with spaces'],
});
/* eslint-enable prettier/prettier */
expect(parse("foo('string with spaces')")).toEqual(
functionEqual('foo', [variableEqual('string with spaces')])
);
});
it('named only', () => {
expect(parse('foo(q=10)')).toEqual(functionEqual('foo', [namedArgumentEqual('q', 10)]));
});
it('named argument is numeric', () => {
expect(parse('foo(q=10.1234e5)')).toEqual(
functionEqual('foo', [namedArgumentEqual('q', 10.1234e5)])
);
});
it('named and positional', () => {
expect(parse('foo(ref, q="bar")')).toEqual(
functionEqual('foo', [variableEqual('ref'), namedArgumentEqual('q', 'bar')])
);
});
it('numerically named', () => {
expect(() => parse('foo(1=2)')).toThrow('but "(" found');
});
it('multiple named', () => {
expect(parse('foo(q_param="bar", offset="1d")')).toEqual(
functionEqual('foo', [
namedArgumentEqual('q_param', 'bar'),
namedArgumentEqual('offset', '1d'),
])
);
});
it('multiple named and positional', () => {
expect(parse('foo(q="bar", ref, offset="1d", 100)')).toEqual(
functionEqual('foo', [
namedArgumentEqual('q', 'bar'),
variableEqual('ref'),
namedArgumentEqual('offset', '1d'),
100,
])
);
});
it('duplicate named', () => {
expect(parse('foo(q="bar", q="test")')).toEqual(
functionEqual('foo', [namedArgumentEqual('q', 'bar'), namedArgumentEqual('q', 'test')])
);
});
it('incomplete named', () => {
expect(() => parse('foo(a=)')).toThrow('but "(" found');
expect(() => parse('foo(=a)')).toThrow('but "(" found');
});
it('invalid named', () => {
expect(() => parse('foo(offset-type="1d")')).toThrow('but "(" found');
});
});
@ -155,7 +215,7 @@ describe('Evaluate', () => {
);
});
it('valiables with dots', () => {
it('variables with dots', () => {
expect(evaluate('foo.bar', { 'foo.bar': 20 })).toEqual(20);
expect(evaluate('"is.null"', { 'is.null': null })).toEqual(null);
expect(evaluate('"is.false"', { 'is.null': null, 'is.false': false })).toEqual(false);
@ -210,6 +270,10 @@ describe('Evaluate', () => {
expect(evaluate('sum("space name")', { 'space name': [1, 2, 21] })).toEqual(24);
});
it('throws on named arguments', () => {
expect(() => evaluate('sum(invalid=a)')).toThrow('Named arguments are not supported');
});
it('equations with injected functions', () => {
expect(
evaluate(

45
packages/kbn-tinymath/tinymath.d.ts vendored Normal file
View file

@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export function parse(expression: string): TinymathAST;
export function evaluate(
expression: string | null,
context: Record<string, any>
): number | number[];
// Named arguments are not top-level parts of the grammar, but can be nested
export type TinymathAST = number | TinymathVariable | TinymathFunction | TinymathNamedArgument;
// Zero-indexed location
export interface TinymathLocation {
min: number;
max: number;
}
export interface TinymathFunction {
type: 'function';
name: string;
text: string;
args: TinymathAST[];
location: TinymathLocation;
}
export interface TinymathVariable {
type: 'variable';
value: string;
text: string;
location: TinymathLocation;
}
export interface TinymathNamedArgument {
type: 'namedArgument';
name: string;
value: string;
text: string;
location: TinymathLocation;
}

View file

@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-tinymath"
},
"include": ["tinymath.d.ts"]
}

View file

@ -134,9 +134,7 @@ describe('math(resp, panel, series)', () => {
series
)(await mathAgg(resp, panel, series)((results) => results))([]);
} catch (e) {
expect(e.message).toEqual(
'Failed to parse expression. Expected "*", "+", "-", "/", or end of input but "(" found.'
);
expect(e.message).toEqual('No such function: notExistingFn');
}
});

View file

@ -5,7 +5,6 @@
* 2.0.
*/
// @ts-expect-error no @typed def; Elastic library
import { evaluate } from '@kbn/tinymath';
import { pivotObjectArray } from '../../../common/lib/pivot_object_array';
import { Datatable, isDatatable, ExpressionFunctionDefinition } from '../../../types';

View file

@ -11,7 +11,7 @@ import { getExpressionType } from './pointseries/lib/get_expression_type';
describe('getExpressionType', () => {
it('returns the result type of an evaluated math expression', () => {
expect(getExpressionType(testTable.columns, '2')).toBe('number');
expect(getExpressionType(testTable.colunns, '2 + 3')).toBe('number');
expect(getExpressionType(testTable.columns, '2 + 3')).toBe('number');
expect(getExpressionType(testTable.columns, 'name')).toBe('string');
expect(getExpressionType(testTable.columns, 'time')).toBe('date');
expect(getExpressionType(testTable.columns, 'price')).toBe('number');

View file

@ -5,7 +5,6 @@
* 2.0.
*/
// @ts-expect-error untyped library
import { parse } from '@kbn/tinymath';
import { getFieldNames } from './pointseries/lib/get_field_names';

View file

@ -5,7 +5,6 @@
* 2.0.
*/
// @ts-expect-error Untyped Elastic library
import { evaluate } from '@kbn/tinymath';
import { groupBy, zipObject, omit, uniqBy } from 'lodash';
import moment from 'moment';
@ -20,7 +19,6 @@ import {
import { pivotObjectArray } from '../../../../common/lib/pivot_object_array';
import { unquoteString } from '../../../../common/lib/unquote_string';
import { isColumnReference } from './lib/is_column_reference';
// @ts-expect-error untyped local
import { getExpressionType } from './lib/get_expression_type';
import { getFunctionHelp, getFunctionErrors } from '../../../../i18n';
@ -132,16 +130,17 @@ export function pointseries(): ExpressionFunctionDefinition<
[PRIMARY_KEY]: i,
}));
function normalizeValue(expression: string, value: string) {
function normalizeValue(expression: string, value: number | number[], index: number) {
const numberValue = Array.isArray(value) ? value[index] : value;
switch (getExpressionType(input.columns, expression)) {
case 'string':
return String(value);
return String(numberValue);
case 'number':
return Number(value);
return Number(numberValue);
case 'date':
return moment(value).valueOf();
return moment(numberValue).valueOf();
default:
return value;
return numberValue;
}
}
@ -153,7 +152,7 @@ export function pointseries(): ExpressionFunctionDefinition<
(acc: Record<string, string | number>, { name, value }) => {
try {
acc[name] = args[name]
? normalizeValue(value, evaluate(value, mathScope)[i])
? normalizeValue(value, evaluate(value, mathScope), i)
: '_all';
} catch (e) {
// TODO: handle invalid column names...

View file

@ -6,11 +6,12 @@
*/
import { parse } from '@kbn/tinymath';
import { DatatableColumn } from 'src/plugins/expressions/common';
import { getFieldType } from '../../../../../common/lib/get_field_type';
import { isColumnReference } from './is_column_reference';
import { getFieldNames } from './get_field_names';
export function getExpressionType(columns, mathExpression) {
export function getExpressionType(columns: DatatableColumn[], mathExpression: string) {
// if isColumnReference returns true, then mathExpression is just a string
// referencing a column in a datatable
if (isColumnReference(mathExpression)) {
@ -19,7 +20,7 @@ export function getExpressionType(columns, mathExpression) {
const parsedMath = parse(mathExpression);
if (parsedMath.args) {
if (typeof parsedMath !== 'number' && parsedMath.type === 'function') {
const fieldNames = parsedMath.args.reduce(getFieldNames, []);
if (fieldNames.length > 0) {
@ -30,7 +31,7 @@ export function getExpressionType(columns, mathExpression) {
}
return types;
}, []);
}, [] as string[]);
return fieldTypes.length === 1 ? fieldTypes[0] : 'string';
}

View file

@ -5,21 +5,19 @@
* 2.0.
*/
type Arg =
| string
| number
| {
name: string;
args: Arg[];
};
import { TinymathAST } from '@kbn/tinymath';
export function getFieldNames(names: string[], arg: Arg): string[] {
if (typeof arg === 'object' && arg.args !== undefined) {
return names.concat(arg.args.reduce(getFieldNames, []));
export function getFieldNames(names: string[], ast: TinymathAST): string[] {
if (typeof ast === 'number') {
return names;
}
if (typeof arg === 'string') {
return names.concat(arg);
if (ast.type === 'function') {
return names.concat(ast.args.reduce(getFieldNames, []));
}
if (ast.type === 'variable') {
return names.concat(ast.value);
}
return names;

View file

@ -5,7 +5,6 @@
* 2.0.
*/
// @ts-expect-error untyped library
import { parse } from '@kbn/tinymath';
export function isColumnReference(mathExpression: string | null): boolean {
@ -13,5 +12,5 @@ export function isColumnReference(mathExpression: string | null): boolean {
mathExpression = 'null';
}
const parsedMath = parse(mathExpression);
return typeof parsedMath === 'string';
return typeof parsedMath !== 'number' && parsedMath.type === 'variable';
}

View file

@ -9,7 +9,7 @@ import { parse } from '@kbn/tinymath';
import { unquoteString } from '../../../../common/lib/unquote_string';
// break out into separate function, write unit tests first
export function getFormObject(argValue) {
export function getFormObject(argValue: string) {
if (argValue === '') {
return {
fn: '',
@ -20,23 +20,28 @@ export function getFormObject(argValue) {
// check if the value is a math expression, and set its type if it is
const mathObj = parse(argValue);
// A symbol node is a plain string, so we guess that they're looking for a column.
if (typeof mathObj === 'string') {
if (typeof mathObj === 'number') {
throw new Error(`Cannot render scalar values or complex math expressions`);
}
if (mathObj.type === 'variable') {
return {
fn: '',
column: unquoteString(argValue),
column: unquoteString(mathObj.value),
};
}
// Check if its a simple function, eg a function wrapping a symbol node
// check for only one arg of type string
if (
typeof mathObj === 'object' &&
mathObj.type === 'function' &&
mathObj.args.length === 1 &&
typeof mathObj.args[0] === 'string'
typeof mathObj.args[0] !== 'number' &&
mathObj.args[0].type === 'variable'
) {
return {
fn: mathObj.name,
column: unquoteString(mathObj.args[0]),
column: unquoteString(mathObj.args[0].value),
};
}