From 534b07d120cb34f40153b497df6d8d9c3eecdd57 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 23 Nov 2016 23:15:30 +0000 Subject: [PATCH] Include jsonata from dependency on build and improve func highlight --- Gruntfile.js | 2 +- editor/images/typedInput/expr.png | Bin 786 -> 563 bytes editor/js/ui/editor.js | 78 +- editor/vendor/jsonata/formatter.js | 40 +- editor/vendor/jsonata/jsonata.js | 2807 ------------------------- editor/vendor/jsonata/mode-jsonata.js | 17 +- 6 files changed, 120 insertions(+), 2824 deletions(-) delete mode 100644 editor/vendor/jsonata/jsonata.js diff --git a/Gruntfile.js b/Gruntfile.js index 9c44b782e..bf6994aca 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -154,7 +154,7 @@ module.exports = function(grunt) { // bootstrap/FA/jquery ], "public/vendor/jsonata/jsonata.js": [ - "editor/vendor/jsonata/jsonata.js", + "node_modules/jsonata/jsonata.js", "editor/vendor/jsonata/formatter.js" ] } diff --git a/editor/images/typedInput/expr.png b/editor/images/typedInput/expr.png index 74d9516aee3c9d64b8f0455bdab7f82b94208433..704105ce5846cbcd5233b418fdecf8bb7b6da5f7 100644 GIT binary patch delta 512 zcmV+b0{{Jz2D1c^J%0h20004*0oOVK{{R302XskIMF-#u7Y-pAg5-ZZ0005NNklQ8+S=1kV4H9fLlN;1lbN|#GHHe} ziapqm!!LC+m7V!#qa1GO$IajnRM?_z>DgC!U-@XBxEO-CF6w<{20000VMMNkR7YeOb+I|VYgC9a^f`1h^y6R&k4M=8)VL;J^ zVsK#>N{lWWnCKu zDEt^To6Q02!K#Y(U3xz5>{k&oT4z*nj{agm~Ch3!-JeB^e1AiA1)T`HKWR zXPV~au>p(4Vt<>6@=BDeV*>&}tJQj>sBUMo*<)h^Rw|W`%=}V{@F+9i8XK?!i7Tqr zzz1Ji044xiQ8R#qWm%sBkFd||5rCDzQ<}@=rT{Dqa6|+ObY1r*5gqSS*BQewt_BKJ z)5*+llufh}Skq*)*)yK!?GjPwU%<>fOGIx0L^`@Z&3|U|wBtBG4;&#gFG(;k^M-BP zADDSdGCQJa+Cm^HRnsYiSnJfTFzih33p7nrC!z}yyw+$mwgI44t8D=IAvKSgPN&Zw z2q=V5XK&qcoIL=jR;w<6r^7@!5SU0LGysdr2z0#|?RGn_9J`mx*id&!}*uIpYO3D|kn<>txvG44ZS T0R{}Q00000NkvXXu0mjfEtEpG diff --git a/editor/js/ui/editor.js b/editor/js/ui/editor.js index 5e9775ec8..915c29649 100644 --- a/editor/js/ui/editor.js +++ b/editor/js/ui/editor.js @@ -1415,7 +1415,7 @@ RED.editor = (function() { var trayBody = tray.find('.editor-tray-body'); var dialogForm = buildEditForm(tray,'dialog-form','_expression','editor'); var funcSelect = $("#node-input-expression-func"); - jsonata.functions.forEach(function(f) { + Object.keys(jsonata.functions).forEach(function(f) { funcSelect.append($("").val(f).text(f)); }) funcSelect.change(function(e) { @@ -1436,14 +1436,75 @@ RED.editor = (function() { enableLiveAutocompletion: true } }); - expressionEditor.getSession().setValue(value||"",-1); + var currentToken = null; + var currentTokenPos = -1; + var currentFunctionMarker = null; + expressionEditor.getSession().setValue(value||"",-1); expressionEditor.on("changeSelection", function() { var c = expressionEditor.getCursorPosition(); var token = expressionEditor.getSession().getTokenAt(c.row,c.column); - // console.log(token); - if (token && token.type === 'keyword') { - funcSelect.val(token.value).change(); + if (token !== currentToken || (token && /paren/.test(token.type) && c.column !== currentTokenPos)) { + currentToken = token; + var r,p; + var scopedFunction = null; + if (token && token.type === 'keyword') { + r = c.row; + scopedFunction = token; + } else { + var depth = 0; + var next = false; + if (token) { + if (token.type === 'paren.rparen') { + // If this is a block of parens ')))', set + // depth to offset against the cursor position + // within the block + currentTokenPos = c.column; + depth = c.column - (token.start + token.value.length); + } + r = c.row; + p = token.index; + } else { + r = c.row-1; + p = -1; + } + while ( scopedFunction === null && r > -1) { + var rowTokens = expressionEditor.getSession().getTokens(r); + if (p === -1) { + p = rowTokens.length-1; + } + while (p > -1) { + var type = rowTokens[p].type; + if (next) { + if (type === 'keyword') { + scopedFunction = rowTokens[p]; + // console.log("HIT",scopedFunction); + break; + } + next = false; + } + if (type === 'paren.lparen') { + depth-=rowTokens[p].value.length; + } else if (type === 'paren.rparen') { + depth+=rowTokens[p].value.length; + } + if (depth < 0) { + next = true; + depth = 0; + } + // console.log(r,p,depth,next,rowTokens[p]); + p--; + } + if (!scopedFunction) { + r--; + } + } + } + expressionEditor.session.removeMarker(currentFunctionMarker); + if (scopedFunction) { + //console.log(token,.map(function(t) { return t.type})); + funcSelect.val(scopedFunction.value).change(); + } } }); @@ -1452,10 +1513,8 @@ RED.editor = (function() { e.preventDefault(); var pos = expressionEditor.getCursorPosition(); var f = funcSelect.val(); - var args = RED._('jsonata:'+f+".args",{defaultValue:''}); - expressionEditor.insert(f+"("+args+")"); - pos.column += f.length+1; - expressionEditor.moveCursorToPosition(pos); + var snippet = jsonata.getFunctionSnippet(f); + expressionEditor.insertSnippet(snippet); expressionEditor.focus(); }) }, @@ -1521,7 +1580,6 @@ RED.editor = (function() { } },100); } - return editor; } } diff --git a/editor/vendor/jsonata/formatter.js b/editor/vendor/jsonata/formatter.js index 036de6852..f0d231a79 100644 --- a/editor/vendor/jsonata/formatter.js +++ b/editor/vendor/jsonata/formatter.js @@ -105,5 +105,43 @@ } jsonata.format = formatExpression; - jsonata.functions = ["$sum", "$count", "$max", "$min", "$average", "$string", "$substring", "$substringBefore", "$substringAfter", "$lowercase", "$uppercase", "$length", "$split", "$join", "$number", "$boolean", "$not", "$map", "$reduce", "$keys", "$lookup", "$append", "$exists", "$spread"] + jsonata.functions = + { + '$append':{ args:['array','array'] }, + '$average':{ args:['value'] }, + '$boolean':{ args:['value'] }, + '$count':{ args:['array'] }, + '$exists':{ args:['value'] }, + '$join':{ args:['array','separator'] }, + '$keys':{ args:['object'] }, + '$length':{ args:['string'] }, + '$lookup':{ args:['object','key'] }, + '$lowercase':{ args:['string'] }, + '$map':{ args:[] }, + '$max':{ args:['array'] }, + '$min':{ args:['array'] }, + '$not':{ args:['value'] }, + '$number':{ args:['value'] }, + '$reduce':{ args:[] }, + '$split':{ args:['string','separator','limit'] }, + '$spread':{ args:['object'] }, + '$string':{ args:['value'] }, + '$substring':{ args:['string','start','length'] }, + '$substringAfter':{ args:['string','chars'] }, + '$substringBefore':{ args:['string','chars'] }, + '$sum':{ args:['array'] }, + '$uppercase':{ args:['string'] } + } + jsonata.getFunctionSnippet = function(fn) { + var snippetText = ""; + if (jsonata.functions.hasOwnProperty(fn)) { + var def = jsonata.functions[fn]; + snippetText = "\\"+fn+"("; + if (def.args) { + snippetText += def.args.map(function(a,i) { return "${"+(i+1)+":"+a+"}"}).join(", "); + } + snippetText += ")\n" + } + return snippetText; + } })(); diff --git a/editor/vendor/jsonata/jsonata.js b/editor/vendor/jsonata/jsonata.js deleted file mode 100644 index 6c4ff877e..000000000 --- a/editor/vendor/jsonata/jsonata.js +++ /dev/null @@ -1,2807 +0,0 @@ -/** - * © Copyright IBM Corp. 2016 All Rights Reserved - * Project name: JSONata - * This project is licensed under the MIT License, see LICENSE - */ - -/** - * @module JSONata - * @description JSON query and transformation language - */ - -/** - * jsonata - * @function - * @param {Object} expr - JSONata expression - * @returns {{evaluate: evaluate, assign: assign}} Evaluated expression - */ -var jsonata = (function() { - 'use strict'; - - var operators = { - '.': 75, - '[': 80, - ']': 0, - '{': 70, - '}': 0, - '(': 80, - ')': 0, - ',': 0, - '@': 75, - '#': 70, - ';': 80, - ':': 80, - '?': 20, - '+': 50, - '-': 50, - '*': 60, - '/': 60, - '%': 60, - '|': 20, - '=': 40, - '<': 40, - '>': 40, - '`': 80, - '**': 60, - '..': 20, - ':=': 10, - '!=': 40, - '<=': 40, - '>=': 40, - 'and': 30, - 'or': 25, - 'in': 40, - '&': 50, - '!': 0 // not an operator, but needed as a stop character for name tokens - }; - - var escapes = { // JSON string escape sequences - see json.org - '"': '"', - '\\': '\\', - '/': '/', - 'b': '\b', - 'f': '\f', - 'n': '\n', - 'r': '\r', - 't': '\t' - }; - - // Tokenizer (lexer) - invoked by the parser to return one token at a time - var tokenizer = function (path) { - var position = 0; - var length = path.length; - - var create = function (type, value) { - var obj = {type: type, value: value, position: position}; - return obj; - }; - - var next = function () { - if (position >= length) return null; - var currentChar = path.charAt(position); - // skip whitespace - while (position < length && ' \t\n\r\v'.indexOf(currentChar) > -1) { - position++; - currentChar = path.charAt(position); - } - // handle double-char operators - if (currentChar === '.' && path.charAt(position + 1) === '.') { - // double-dot .. range operator - position += 2; - return create('operator', '..'); - } - if (currentChar === ':' && path.charAt(position + 1) === '=') { - // := assignment - position += 2; - return create('operator', ':='); - } - if (currentChar === '!' && path.charAt(position + 1) === '=') { - // != - position += 2; - return create('operator', '!='); - } - if (currentChar === '>' && path.charAt(position + 1) === '=') { - // >= - position += 2; - return create('operator', '>='); - } - if (currentChar === '<' && path.charAt(position + 1) === '=') { - // <= - position += 2; - return create('operator', '<='); - } - if (currentChar === '*' && path.charAt(position + 1) === '*') { - // ** descendant wildcard - position += 2; - return create('operator', '**'); - } - // test for operators - if (operators.hasOwnProperty(currentChar)) { - position++; - return create('operator', currentChar); - } - // test for string literals - if (currentChar === '"' || currentChar === "'") { - var quoteType = currentChar; - // double quoted string literal - find end of string - position++; - var qstr = ""; - while (position < length) { - currentChar = path.charAt(position); - if (currentChar === '\\') { // escape sequence - position++; - currentChar = path.charAt(position); - if (escapes.hasOwnProperty(currentChar)) { - qstr += escapes[currentChar]; - } else if (currentChar === 'u') { - // \u should be followed by 4 hex digits - var octets = path.substr(position + 1, 4); - if (/^[0-9a-fA-F]+$/.test(octets)) { - var codepoint = parseInt(octets, 16); - qstr += String.fromCharCode(codepoint); - position += 4; - } else { - throw { - message: "The escape sequence \\u must be followed by 4 hex digits", - stack: (new Error()).stack, - position: position - }; - } - } else { - // illegal escape sequence - throw { - message: 'unsupported escape sequence: \\' + currentChar, - stack: (new Error()).stack, - position: position, - token: currentChar - }; - - } - } else if (currentChar === quoteType) { - position++; - return create('string', qstr); - } else { - qstr += currentChar; - } - position++; - } - throw { - message: 'no terminating quote found in string literal', - stack: (new Error()).stack, - position: position - }; - } - // test for numbers - var numregex = /^-?(0|([1-9][0-9]*))(\.[0-9]+)?([Ee][-+]?[0-9]+)?/; - var match = numregex.exec(path.substring(position)); - if (match !== null) { - var num = parseFloat(match[0]); - if (!isNaN(num) && isFinite(num)) { - position += match[0].length; - return create('number', num); - } else { - throw { - message: 'Number out of range: ' + match[0], - stack: (new Error()).stack, - position: position, - token: match[0] - }; - } - } - // test for names - var i = position; - var ch; - var name; - for (;;) { - ch = path.charAt(i); - if (i == length || ' \t\n\r\v'.indexOf(ch) > -1 || operators.hasOwnProperty(ch)) { - if (path.charAt(position) === '$') { - // variable reference - name = path.substring(position + 1, i); - position = i; - return create('variable', name); - } else { - name = path.substring(position, i); - position = i; - switch (name) { - case 'and': - case 'or': - case 'in': - return create('operator', name); - case 'true': - return create('value', true); - case 'false': - return create('value', false); - case 'null': - return create('value', null); - default: - if (position == length && name === '') { - // whitespace at end of input - return null; - } - return create('name', name); - } - } - } else { - i++; - } - } - }; - - return next; - }; - - - // This parser implements the 'Top down operator precedence' algorithm developed by Vaughan R Pratt; http://dl.acm.org/citation.cfm?id=512931. - // and builds on the Javascript framework described by Douglas Crockford at http://javascript.crockford.com/tdop/tdop.html - // and in 'Beautiful Code', edited by Andy Oram and Greg Wilson, Copyright 2007 O'Reilly Media, Inc. 798-0-596-51004-6 - - var parser = function (source) { - var node; - var lexer; - - var symbol_table = {}; - - var base_symbol = { - nud: function () { - return this; - } - }; - - var symbol = function (id, bp) { - var s = symbol_table[id]; - bp = bp || 0; - if (s) { - if (bp >= s.lbp) { - s.lbp = bp; - } - } else { - s = Object.create(base_symbol); - s.id = s.value = id; - s.lbp = bp; - symbol_table[id] = s; - } - return s; - }; - - var advance = function (id) { - if (id && node.id !== id) { - var msg; - if(node.id === '(end)') { - // unexpected end of buffer - msg = "Syntax error: expected '" + id + "' before end of expression"; - } else { - msg = "Syntax error: expected '" + id + "', got '" + node.id; - } - throw { - message: msg , - stack: (new Error()).stack, - position: node.position, - token: node.id, - value: id - }; - } - var next_token = lexer(); - if (next_token === null) { - node = symbol_table["(end)"]; - return node; - } - var value = next_token.value; - var type = next_token.type; - var symbol; - switch (type) { - case 'name': - case 'variable': - symbol = symbol_table["(name)"]; - break; - case 'operator': - symbol = symbol_table[value]; - if (!symbol) { - throw { - message: "Unknown operator: " + value, - stack: (new Error()).stack, - position: next_token.position, - token: value - }; - } - break; - case 'string': - case 'number': - case 'value': - type = "literal"; - symbol = symbol_table["(literal)"]; - break; - /* istanbul ignore next */ - default: - throw { - message: "Unexpected token:" + value, - stack: (new Error()).stack, - position: next_token.position, - token: value - }; - } - - node = Object.create(symbol); - node.value = value; - node.type = type; - node.position = next_token.position; - return node; - }; - - // Pratt's algorithm - var expression = function (rbp) { - var left; - var t = node; - advance(); - left = t.nud(); - while (rbp < node.lbp) { - t = node; - advance(); - left = t.led(left); - } - return left; - }; - - // match infix operators - // - // left associative - var infix = function (id, bp, led) { - var bindingPower = bp || operators[id]; - var s = symbol(id, bindingPower); - s.led = led || function (left) { - this.lhs = left; - this.rhs = expression(bindingPower); - this.type = "binary"; - return this; - }; - return s; - }; - - // match infix operators - // - // right associative - var infixr = function (id, bp, led) { - var bindingPower = bp || operators[id]; - var s = symbol(id, bindingPower); - s.led = led || function (left) { - this.lhs = left; - this.rhs = expression(bindingPower - 1); // subtract 1 from bindingPower for right associative operators - this.type = "binary"; - return this; - }; - return s; - }; - - // match prefix operators - // - var prefix = function (id, nud) { - var s = symbol(id); - s.nud = nud || function () { - this.expression = expression(70); - this.type = "unary"; - return this; - }; - return s; - }; - - symbol("(end)"); - symbol("(name)"); - symbol("(literal)"); - symbol(":"); - symbol(";"); - symbol(","); - symbol(")"); - symbol("]"); - symbol("}"); - symbol(".."); // range operator - infix("."); // field reference - infix("+"); // numeric addition - infix("-"); // numeric subtraction - infix("*"); // numeric multiplication - infix("/"); // numeric division - infix("%"); // numeric modulus - infix("="); // equality - infix("<"); // less than - infix(">"); // greater than - infix("!="); // not equal to - infix("<="); // less than or equal - infix(">="); // greater than or equal - infix("&"); // string concatenation - infix("and"); // Boolean AND - infix("or"); // Boolean OR - infix("in"); // is member of array - infixr(":="); // bind variable - prefix("-"); // unary numeric negation - - // field wildcard (single level) - prefix('*', function () { - this.type = "wildcard"; - return this; - }); - - // descendant wildcard (multi-level) - prefix('**', function () { - this.type = "descendant"; - return this; - }); - - // function invocation - infix("(", operators['('], function (left) { - // left is is what we are trying to invoke - this.procedure = left; - this.type = 'function'; - this.arguments = []; - if (node.id !== ')') { - for (;;) { - if (node.type === 'operator' && node.id === '?') { - // partial function application - this.type = 'partial'; - this.arguments.push(node); - advance('?'); - } else { - this.arguments.push(expression(0)); - } - if (node.id !== ',') break; - advance(','); - } - } - advance(")"); - // if the name of the function is 'function' or λ, then this is function definition (lambda function) - if (left.type === 'name' && (left.value === 'function' || left.value === '\u03BB')) { - // all of the args must be VARIABLE tokens - this.arguments.forEach(function (arg, index) { - if (arg.type !== 'variable') { - throw { - message: 'Parameter ' + (index + 1) + ' of function definition must be a variable name (start with $)', - stack: (new Error()).stack, - position: arg.position, - token: arg.value - }; - } - }); - this.type = 'lambda'; - // parse the function body - advance('{'); - this.body = expression(0); - advance('}'); - } - return this; - }); - - // parenthesis - block expression - prefix("(", function () { - var expressions = []; - while (node.id !== ")") { - expressions.push(expression(0)); - if (node.id !== ";") { - break; - } - advance(";"); - } - advance(")"); - this.type = 'block'; - this.expressions = expressions; - return this; - }); - - // array constructor - prefix("[", function () { - var a = []; - if (node.id !== "]") { - for (;;) { - var item = expression(0); - if (node.id === "..") { - // range operator - var range = {type: "binary", value: "..", position: node.position, lhs: item}; - advance(".."); - range.rhs = expression(0); - item = range; - } - a.push(item); - if (node.id !== ",") { - break; - } - advance(","); - } - } - advance("]"); - this.lhs = a; - this.type = "unary"; - return this; - }); - - // filter - predicate or array index - infix("[", operators['['], function (left) { - if(node.id === "]") { - // empty predicate means maintain singleton arrays in the output - var step = left; - while(step && step.type === 'binary' && step.value === '[') { - step = step.lhs; - } - step.keepArray = true; - advance("]"); - return left; - } else { - this.lhs = left; - this.rhs = expression(operators[']']); - this.type = 'binary'; - advance("]"); - return this; - } - }); - - var objectParser = function (left) { - var a = []; - if (node.id !== "}") { - for (;;) { - var n = expression(0); - advance(":"); - var v = expression(0); - a.push([n, v]); // holds an array of name/value expression pairs - if (node.id !== ",") { - break; - } - advance(","); - } - } - advance("}"); - if(typeof left === 'undefined') { - // NUD - unary prefix form - this.lhs = a; - this.type = "unary"; - } else { - // LED - binary infix form - this.lhs = left; - this.rhs = a; - this.type = 'binary'; - } - return this; - }; - - // object constructor - prefix("{", objectParser); - - // object grouping - infix("{", operators['{'], objectParser); - - // if/then/else ternary operator ?: - infix("?", operators['?'], function (left) { - this.type = 'condition'; - this.condition = left; - this.then = expression(0); - if (node.id === ':') { - // else condition - advance(":"); - this.else = expression(0); - } - return this; - }); - - // tail call optimization - // this is invoked by the post parser to analyse lambda functions to see - // if they make a tail call. If so, it is replaced by a thunk which will - // be invoked by the trampoline loop during function application. - // This enables tail-recursive functions to be written without growing the stack - var tail_call_optimize = function(expr) { - var result; - if(expr.type === 'function') { - var thunk = {type: 'lambda', thunk: true, arguments: [], position: expr.position}; - thunk.body = expr; - result = thunk; - } else if(expr.type === 'condition') { - // analyse both branches - expr.then = tail_call_optimize(expr.then); - expr.else = tail_call_optimize(expr.else); - result = expr; - } else if(expr.type === 'block') { - // only the last expression in the block - var length = expr.expressions.length; - if(length > 0) { - expr.expressions[length - 1] = tail_call_optimize(expr.expressions[length - 1]); - } - result = expr; - } else { - result = expr; - } - return result; - }; - - // post-parse stage - // the purpose of this is flatten the parts of the AST representing location paths, - // converting them to arrays of steps which in turn may contain arrays of predicates. - // following this, nodes containing '.' and '[' should be eliminated from the AST. - var post_parse = function (expr) { - var result = []; - switch (expr.type) { - case 'binary': - switch (expr.value) { - case '.': - var lstep = post_parse(expr.lhs); - if (lstep.type === 'path') { - Array.prototype.push.apply(result, lstep); - } else { - result.push(lstep); - } - var rest = post_parse(expr.rhs); - if(rest.type !== 'path') { - rest = [rest]; - } - Array.prototype.push.apply(result, rest); - result.type = 'path'; - break; - case '[': - // predicated step - // LHS is a step or a predicated step - // RHS is the predicate expr - result = post_parse(expr.lhs); - var step = result; - if(result.type === 'path') { - step = result[result.length - 1]; - } - if (typeof step.group !== 'undefined') { - throw { - message: 'A predicate cannot follow a grouping expression in a step. Error at column: ' + expr.position, - stack: (new Error()).stack, - position: expr.position - }; - } - if (typeof step.predicate === 'undefined') { - step.predicate = []; - } - step.predicate.push(post_parse(expr.rhs)); - break; - case '{': - // group-by - // LHS is a step or a predicated step - // RHS is the object constructor expr - result = post_parse(expr.lhs); - if (typeof result.group !== 'undefined') { - throw { - message: 'Each step can only have one grouping expression. Error at column: ' + expr.position, - stack: (new Error()).stack, - position: expr.position - }; - } - // object constructor - process each pair - result.group = { - lhs: expr.rhs.map(function (pair) { - return [post_parse(pair[0]), post_parse(pair[1])]; - }), - position: expr.position - }; - break; - default: - result = {type: expr.type, value: expr.value, position: expr.position}; - result.lhs = post_parse(expr.lhs); - result.rhs = post_parse(expr.rhs); - } - break; - case 'unary': - result = {type: expr.type, value: expr.value, position: expr.position}; - if (expr.value === '[') { - // array constructor - process each item - result.lhs = expr.lhs.map(function (item) { - return post_parse(item); - }); - } else if (expr.value === '{') { - // object constructor - process each pair - result.lhs = expr.lhs.map(function (pair) { - return [post_parse(pair[0]), post_parse(pair[1])]; - }); - } else { - // all other unary expressions - just process the expression - result.expression = post_parse(expr.expression); - // if unary minus on a number, then pre-process - if (expr.value === '-' && result.expression.type === 'literal' && isNumeric(result.expression.value)) { - result = result.expression; - result.value = -result.value; - } - } - break; - case 'function': - case 'partial': - result = {type: expr.type, name: expr.name, value: expr.value, position: expr.position}; - result.arguments = expr.arguments.map(function (arg) { - return post_parse(arg); - }); - result.procedure = post_parse(expr.procedure); - break; - case 'lambda': - result = {type: expr.type, arguments: expr.arguments, position: expr.position}; - var body = post_parse(expr.body); - result.body = tail_call_optimize(body); - break; - case 'condition': - result = {type: expr.type, position: expr.position}; - result.condition = post_parse(expr.condition); - result.then = post_parse(expr.then); - if (typeof expr.else !== 'undefined') { - result.else = post_parse(expr.else); - } - break; - case 'block': - result = {type: expr.type, position: expr.position}; - // array of expressions - process each one - result.expressions = expr.expressions.map(function (item) { - return post_parse(item); - }); - // TODO scan the array of expressions to see if any of them assign variables - // if so, need to mark the block as one that needs to create a new frame - break; - case 'name': - result = [expr]; - result.type = 'path'; - break; - case 'literal': - case 'wildcard': - case 'descendant': - case 'variable': - result = expr; - break; - case 'operator': - // the tokens 'and' and 'or' might have been used as a name rather than an operator - if (expr.value === 'and' || expr.value === 'or' || expr.value === 'in') { - expr.type = 'name'; - result = post_parse(expr); - } else if (expr.value === '?') { - // partial application - result = expr; - } else { - throw { - message: "Syntax error: " + expr.value, - stack: (new Error()).stack, - position: expr.position, - token: expr.value - }; - } - break; - default: - var reason = "Unknown expression type: " + expr.value; - /* istanbul ignore else */ - if (expr.id === '(end)') { - reason = "Syntax error: unexpected end of expression"; - } - throw { - message: reason, - stack: (new Error()).stack, - position: expr.position, - token: expr.value - }; - } - return result; - }; - - // now invoke the tokenizer and the parser and return the syntax tree - - lexer = tokenizer(source); - advance(); - // parse the tokens - var expr = expression(0); - if (node.id !== '(end)') { - throw { - message: "Syntax error: " + node.value, - stack: (new Error()).stack, - position: node.position, - token: node.value - }; - } - expr = post_parse(expr); - - return expr; - }; - - /** - * Check if value is a finite number - * @param {float} n - number to evaluate - * @returns {boolean} True if n is a finite number - */ - function isNumeric(n) { - var isNum = false; - if(typeof n === 'number') { - var num = parseFloat(n); - isNum = !isNaN(num); - if (isNum && !isFinite(num)) { - throw { - message: "Number out of range", - value: n, - stack: (new Error()).stack - }; - } - } - return isNum; - } - - /** - * Returns true if the arg is an array of numbers - * @param {*} arg - the item to test - * @returns {boolean} True if arg is an array of numbers - */ - function isArrayOfNumbers(arg) { - var result = false; - if(Array.isArray(arg)) { - result = (arg.filter(function(item){return !isNumeric(item);}).length == 0); - } - return result; - } - - // Polyfill - /* istanbul ignore next */ - Number.isInteger = Number.isInteger || function(value) { - return typeof value === "number" && - isFinite(value) && - Math.floor(value) === value; - }; - - /** - * Evaluate expression against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluate(expr, input, environment) { - var result; - - var entryCallback = environment.lookup('__evaluate_entry'); - if(entryCallback) { - entryCallback(expr, input, environment); - } - - switch (expr.type) { - case 'path': - result = evaluatePath(expr, input, environment); - break; - case 'binary': - result = evaluateBinary(expr, input, environment); - break; - case 'unary': - result = evaluateUnary(expr, input, environment); - break; - case 'name': - result = evaluateName(expr, input, environment); - break; - case 'literal': - result = evaluateLiteral(expr, input, environment); - break; - case 'wildcard': - result = evaluateWildcard(expr, input, environment); - break; - case 'descendant': - result = evaluateDescendants(expr, input, environment); - break; - case 'condition': - result = evaluateCondition(expr, input, environment); - break; - case 'block': - result = evaluateBlock(expr, input, environment); - break; - case 'function': - result = evaluateFunction(expr, input, environment); - break; - case 'variable': - result = evaluateVariable(expr, input, environment); - break; - case 'lambda': - result = evaluateLambda(expr, input, environment); - break; - case 'partial': - result = evaluatePartialApplication(expr, input, environment); - break; - } - if (expr.hasOwnProperty('predicate')) { - result = applyPredicates(expr.predicate, result, environment); - } - if (expr.hasOwnProperty('group')) { - result = evaluateGroupExpression(expr.group, result, environment); - } - - var exitCallback = environment.lookup('__evaluate_exit'); - if(exitCallback) { - exitCallback(expr, input, environment, result); - } - - return result; - } - - /** - * Evaluate path expression against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluatePath(expr, input, environment) { - var result; - var inputSequence; - var keepSingletonArray = false; - // expr is an array of steps - // if the first step is a variable reference ($...), including root reference ($$), - // then the path is absolute rather than relative - if (expr[0].type === 'variable') { - expr[0].absolute = true; - } else if(expr[0].type === 'unary' && expr[0].value === '[') { - // array constructor - not relative to the input - input = [null];// dummy singleton sequence for first step - } - - // evaluate each step in turn - for(var ii = 0; ii < expr.length; ii++) { - var step = expr[ii]; - if(step.keepArray === true) { - keepSingletonArray = true; - } - var resultSequence = []; - result = undefined; - // if input is not an array, make it so - if (step.absolute === true) { - inputSequence = [input]; // dummy singleton sequence for first (absolute) step - } else if (Array.isArray(input)) { - inputSequence = input; - } else { - inputSequence = [input]; - } - // if there is more than one step in the path, handle quoted field names as names not literals - if (expr.length > 1 && step.type === 'literal') { - step.type = 'name'; - } - inputSequence.forEach(function (item) { - var res = evaluate(step, item, environment); - if (typeof res !== 'undefined') { - if (Array.isArray(res) && (step.value !== '[' )) { - // is res an array - if so, flatten it into the parent array - res.forEach(function (innerRes) { - if (typeof innerRes !== 'undefined') { - resultSequence.push(innerRes); - } - }); - } else { - resultSequence.push(res); - } - } - }); - if (resultSequence.length == 1) { - if(keepSingletonArray) { - result = resultSequence; - } else { - result = resultSequence[0]; - } - } else if (resultSequence.length > 1) { - result = resultSequence; - } - - if(typeof result === 'undefined') { - break; - } - input = result; - } - return result; - } - - /** - * Apply predicates to input data - * @param {Object} predicates - Predicates - * @param {Object} input - Input data to apply predicates against - * @param {Object} environment - Environment - * @returns {*} Result after applying predicates - */ - function applyPredicates(predicates, input, environment) { - var result; - var inputSequence = input; - // lhs potentially holds an array - // we want to iterate over the array, and only keep the items that are - // truthy when applied to the predicate. - // if the predicate evaluates to an integer, then select that index - - var results = []; - predicates.forEach(function (predicate) { - // if it's not an array, turn it into one - // since in XPath >= 2.0 an item is equivalent to a singleton sequence of that item - // if input is not an array, make it so - if (!Array.isArray(inputSequence)) { - inputSequence = [inputSequence]; - } - results = []; - result = undefined; - if (predicate.type === 'literal' && isNumeric(predicate.value)) { - var index = predicate.value; - if (!Number.isInteger(index)) { - // round it down - index = Math.floor(index); - } - if (index < 0) { - // count in from end of array - index = inputSequence.length + index; - } - result = inputSequence[index]; - } else { - inputSequence.forEach(function (item, index) { - var res = evaluate(predicate, item, environment); - if (isNumeric(res)) { - res = [res]; - } - if(isArrayOfNumbers(res)) { - res.forEach(function(ires) { - if (!Number.isInteger(ires)) { - // round it down - ires = Math.floor(ires); - } - if (ires < 0) { - // count in from end of array - ires = inputSequence.length + ires; - } - if (ires == index) { - results.push(item); - } - }); - } else if (functionBoolean(res)) { // truthy - results.push(item); - } - }); - } - if (results.length == 1) { - result = results[0]; - } else if (results.length > 1) { - result = results; - } - inputSequence = result; - }); - return result; - } - - /** - * Evaluate binary expression against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateBinary(expr, input, environment) { - var result; - - switch (expr.value) { - case '+': - case '-': - case '*': - case '/': - case '%': - result = evaluateNumericExpression(expr, input, environment); - break; - case '=': - case '!=': - case '<': - case '<=': - case '>': - case '>=': - result = evaluateComparisonExpression(expr, input, environment); - break; - case '&': - result = evaluateStringConcat(expr, input, environment); - break; - case 'and': - case 'or': - result = evaluateBooleanExpression(expr, input, environment); - break; - case '..': - result = evaluateRangeExpression(expr, input, environment); - break; - case ':=': - result = evaluateBindExpression(expr, input, environment); - break; - case 'in': - result = evaluateIncludesExpression(expr, input, environment); - break; - } - return result; - } - - /** - * Evaluate unary expression against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateUnary(expr, input, environment) { - var result; - - switch (expr.value) { - case '-': - result = evaluate(expr.expression, input, environment); - if (isNumeric(result)) { - result = -result; - } else { - throw { - message: "Cannot negate a non-numeric value: " + result, - stack: (new Error()).stack, - position: expr.position, - token: expr.value, - value: result - }; - } - break; - case '[': - // array constructor - evaluate each item - result = []; - expr.lhs.forEach(function (item) { - var value = evaluate(item, input, environment); - if (typeof value !== 'undefined') { - if(item.value === '[') { - result.push(value); - } else { - result = functionAppend(result, value); - } - } - }); - break; - case '{': - // object constructor - apply grouping - result = evaluateGroupExpression(expr, input, environment); - break; - - } - return result; - } - - /** - * Evaluate name object against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateName(expr, input, environment) { - // lookup the 'name' item in the input - var result; - if (Array.isArray(input)) { - var results = []; - input.forEach(function (item) { - var res = evaluateName(expr, item, environment); - if (typeof res !== 'undefined') { - results.push(res); - } - }); - if (results.length == 1) { - result = results[0]; - } else if (results.length > 1) { - result = results; - } - } else if (input !== null && typeof input === 'object') { - result = input[expr.value]; - } - return result; - } - - /** - * Evaluate literal against input data - * @param {Object} expr - JSONata expression - * @returns {*} Evaluated input data - */ - function evaluateLiteral(expr) { - return expr.value; - } - - /** - * Evaluate wildcard against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @returns {*} Evaluated input data - */ - function evaluateWildcard(expr, input) { - var result; - var results = []; - if (input !== null && typeof input === 'object') { - Object.keys(input).forEach(function (key) { - var value = input[key]; - if(Array.isArray(value)) { - value = flatten(value); - results = functionAppend(results, value); - } else { - results.push(value); - } - }); - } - - if (results.length == 1) { - result = results[0]; - } else if (results.length > 1) { - result = results; - } - return result; - } - - /** - * Returns a flattened array - * @param {Array} arg - the array to be flatten - * @param {Array} flattened - carries the flattened array - if not defined, will initialize to [] - * @returns {Array} - the flattened array - */ - function flatten(arg, flattened) { - if(typeof flattened === 'undefined') { - flattened = []; - } - if(Array.isArray(arg)) { - arg.forEach(function (item) { - flatten(item, flattened); - }); - } else { - flattened.push(arg); - } - return flattened; - } - - /** - * Evaluate descendants against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @returns {*} Evaluated input data - */ - function evaluateDescendants(expr, input) { - var result; - var resultSequence = []; - if (typeof input !== 'undefined') { - // traverse all descendants of this object/array - recurseDescendants(input, resultSequence); - if (resultSequence.length == 1) { - result = resultSequence[0]; - } else { - result = resultSequence; - } - } - return result; - } - - /** - * Recurse through descendants - * @param {Object} input - Input data - * @param {Object} results - Results - */ - function recurseDescendants(input, results) { - // this is the equivalent of //* in XPath - if (!Array.isArray(input)) { - results.push(input); - } - if (Array.isArray(input)) { - input.forEach(function (member) { - recurseDescendants(member, results); - }); - } else if (input !== null && typeof input === 'object') { - Object.keys(input).forEach(function (key) { - recurseDescendants(input[key], results); - }); - } - } - - /** - * Evaluate numeric expression against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateNumericExpression(expr, input, environment) { - var result; - - var lhs = evaluate(expr.lhs, input, environment); - var rhs = evaluate(expr.rhs, input, environment); - - if (typeof lhs === 'undefined' || typeof rhs === 'undefined') { - // if either side is undefined, the result is undefined - return result; - } - - if (!isNumeric(lhs)) { - throw { - message: 'LHS of ' + expr.value + ' operator must evaluate to a number', - stack: (new Error()).stack, - position: expr.position, - token: expr.value, - value: lhs - }; - } - if (!isNumeric(rhs)) { - throw { - message: 'RHS of ' + expr.value + ' operator must evaluate to a number', - stack: (new Error()).stack, - position: expr.position, - token: expr.value, - value: rhs - }; - } - - switch (expr.value) { - case '+': - result = lhs + rhs; - break; - case '-': - result = lhs - rhs; - break; - case '*': - result = lhs * rhs; - break; - case '/': - result = lhs / rhs; - break; - case '%': - result = lhs % rhs; - break; - } - return result; - } - - /** - * Evaluate comparison expression against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateComparisonExpression(expr, input, environment) { - var result; - - var lhs = evaluate(expr.lhs, input, environment); - var rhs = evaluate(expr.rhs, input, environment); - - if (typeof lhs === 'undefined' || typeof rhs === 'undefined') { - // if either side is undefined, the result is false - return false; - } - - switch (expr.value) { - case '=': - result = lhs == rhs; - break; - case '!=': - result = (lhs != rhs); - break; - case '<': - result = lhs < rhs; - break; - case '<=': - result = lhs <= rhs; - break; - case '>': - result = lhs > rhs; - break; - case '>=': - result = lhs >= rhs; - break; - } - return result; - } - - /** - * Inclusion operator - in - * - * @param {Object} expr - AST - * @param {*} input - input context - * @param {Object} environment - frame - * @returns {boolean} - true if lhs is a member of rhs - */ - function evaluateIncludesExpression(expr, input, environment) { - var result = false; - - var lhs = evaluate(expr.lhs, input, environment); - var rhs = evaluate(expr.rhs, input, environment); - - if (typeof lhs === 'undefined' || typeof rhs === 'undefined') { - // if either side is undefined, the result is false - return false; - } - - if(!Array.isArray(rhs)) { - rhs = [rhs]; - } - - for(var i = 0; i < rhs.length; i++) { - if(rhs[i] === lhs) { - result = true; - break; - } - } - - return result; - } - - /** - * Evaluate boolean expression against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateBooleanExpression(expr, input, environment) { - var result; - - switch (expr.value) { - case 'and': - result = functionBoolean(evaluate(expr.lhs, input, environment)) && - functionBoolean(evaluate(expr.rhs, input, environment)); - break; - case 'or': - result = functionBoolean(evaluate(expr.lhs, input, environment)) || - functionBoolean(evaluate(expr.rhs, input, environment)); - break; - } - return result; - } - - /** - * Evaluate string concatenation against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {string|*} Evaluated input data - */ - function evaluateStringConcat(expr, input, environment) { - var result; - var lhs = evaluate(expr.lhs, input, environment); - var rhs = evaluate(expr.rhs, input, environment); - - var lstr = ''; - var rstr = ''; - if (typeof lhs !== 'undefined') { - lstr = functionString(lhs); - } - if (typeof rhs !== 'undefined') { - rstr = functionString(rhs); - } - - result = lstr.concat(rstr); - return result; - } - - /** - * Evaluate group expression against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {{}} Evaluated input data - */ - function evaluateGroupExpression(expr, input, environment) { - var result = {}; - var groups = {}; - // group the input sequence by 'key' expression - if (!Array.isArray(input)) { - input = [input]; - } - input.forEach(function (item) { - expr.lhs.forEach(function (pair) { - var key = evaluate(pair[0], item, environment); - // key has to be a string - if (typeof key !== 'string') { - throw { - message: 'Key in object structure must evaluate to a string. Got: ' + key, - stack: (new Error()).stack, - position: expr.position, - value: key - }; - } - var entry = {data: item, expr: pair[1]}; - if (groups.hasOwnProperty(key)) { - // a value already exists in this slot - // append it as an array - groups[key].data = functionAppend(groups[key].data, item); - } else { - groups[key] = entry; - } - }); - }); - // iterate over the groups to evaluate the 'value' expression - for (var key in groups) { - var entry = groups[key]; - var value = evaluate(entry.expr, entry.data, environment); - result[key] = value; - } - return result; - } - - /** - * Evaluate range expression against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateRangeExpression(expr, input, environment) { - var result; - - var lhs = evaluate(expr.lhs, input, environment); - var rhs = evaluate(expr.rhs, input, environment); - - if (typeof lhs === 'undefined' || typeof rhs === 'undefined') { - // if either side is undefined, the result is undefined - return result; - } - - if (lhs > rhs) { - // if the lhs is greater than the rhs, return undefined - return result; - } - - if (!Number.isInteger(lhs)) { - throw { - message: 'LHS of range operator (..) must evaluate to an integer', - stack: (new Error()).stack, - position: expr.position, - token: expr.value, - value: lhs - }; - } - if (!Number.isInteger(rhs)) { - throw { - message: 'RHS of range operator (..) must evaluate to an integer', - stack: (new Error()).stack, - position: expr.position, - token: expr.value, - value: rhs - }; - } - - result = new Array(rhs - lhs + 1); - for (var item = lhs, index = 0; item <= rhs; item++, index++) { - result[index] = item; - } - return result; - } - - /** - * Evaluate bind expression against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateBindExpression(expr, input, environment) { - // The RHS is the expression to evaluate - // The LHS is the name of the variable to bind to - should be a VARIABLE token - var value = evaluate(expr.rhs, input, environment); - if (expr.lhs.type !== 'variable') { - throw { - message: "Left hand side of := must be a variable name (start with $)", - stack: (new Error()).stack, - position: expr.position, - token: expr.value, - value: expr.lhs.type === 'path' ? expr.lhs[0].value : expr.lhs.value - }; - } - environment.bind(expr.lhs.value, value); - return value; - } - - /** - * Evaluate condition against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateCondition(expr, input, environment) { - var result; - var condition = evaluate(expr.condition, input, environment); - if (functionBoolean(condition)) { - result = evaluate(expr.then, input, environment); - } else if (typeof expr.else !== 'undefined') { - result = evaluate(expr.else, input, environment); - } - return result; - } - - /** - * Evaluate block against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateBlock(expr, input, environment) { - var result; - // create a new frame to limit the scope of variable assignments - // TODO, only do this if the post-parse stage has flagged this as required - var frame = createFrame(environment); - // invoke each expression in turn - // only return the result of the last one - expr.expressions.forEach(function (expression) { - result = evaluate(expression, input, frame); - }); - - return result; - } - - /** - * Evaluate variable against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateVariable(expr, input, environment) { - // lookup the variable value in the environment - var result; - // if the variable name is empty string, then it refers to context value - if (expr.value === '') { - result = input; - } else { - result = environment.lookup(expr.value); - } - return result; - } - - /** - * Evaluate function against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluateFunction(expr, input, environment) { - var result; - // evaluate the arguments - var evaluatedArgs = []; - expr.arguments.forEach(function (arg) { - evaluatedArgs.push(evaluate(arg, input, environment)); - }); - // lambda function on lhs - // create the procedure - // can't assume that expr.procedure is a lambda type directly - // could be an expression that evaluates to a function (e.g. variable reference, parens expr etc. - // evaluate it generically first, then check that it is a function. Throw error if not. - var proc = evaluate(expr.procedure, input, environment); - - if (typeof proc === 'undefined' && expr.procedure.type === 'path' && environment.lookup(expr.procedure[0].value)) { - // help the user out here if they simply forgot the leading $ - throw { - message: 'Attempted to invoke a non-function. Did you mean \'$' + expr.procedure[0].value + '\'?', - stack: (new Error()).stack, - position: expr.position, - token: expr.procedure[0].value - }; - } - // apply the procedure - try { - result = apply(proc, evaluatedArgs, environment, input); - while(typeof result === 'object' && result.lambda == true && result.thunk == true) { - // trampoline loop - this gets invoked as a result of tail-call optimization - // the function returned a tail-call thunk - // unpack it, evaluate its arguments, and apply the tail call - var next = evaluate(result.body.procedure, result.input, result.environment); - evaluatedArgs = []; - result.body.arguments.forEach(function (arg) { - evaluatedArgs.push(evaluate(arg, result.input, result.environment)); - }); - - result = apply(next, evaluatedArgs); - } - } catch (err) { - // add the position field to the error - err.position = expr.position; - // and the function identifier - err.token = expr.procedure.type === 'path' ? expr.procedure[0].value : expr.procedure.value; - throw err; - } - return result; - } - - /** - * Apply procedure or function - * @param {Object} proc - Procedure - * @param {Array} args - Arguments - * @param {Object} environment - Environment - * @param {Object} self - Self - * @returns {*} Result of procedure - */ - function apply(proc, args, environment, self) { - var result; - if (proc && proc.lambda) { - result = applyProcedure(proc, args); - } else if (typeof proc === 'function') { - result = proc.apply(self, args); - } else { - throw { - message: 'Attempted to invoke a non-function', - stack: (new Error()).stack - }; - } - return result; - } - - /** - * Evaluate lambda against input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {{lambda: boolean, input: *, environment: *, arguments: *, body: *}} Evaluated input data - */ - function evaluateLambda(expr, input, environment) { - // make a function (closure) - var procedure = { - lambda: true, - input: input, - environment: environment, - arguments: expr.arguments, - body: expr.body - }; - if(expr.thunk == true) { - procedure.thunk = true; - } - return procedure; - } - - /** - * Evaluate partial application - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {*} Evaluated input data - */ - function evaluatePartialApplication(expr, input, environment) { - // partially apply a function - var result; - // evaluate the arguments - var evaluatedArgs = []; - expr.arguments.forEach(function (arg) { - if (arg.type === 'operator' && arg.value === '?') { - evaluatedArgs.push(arg); - } else { - evaluatedArgs.push(evaluate(arg, input, environment)); - } - }); - // lookup the procedure - var proc = evaluate(expr.procedure, input, environment); - if (typeof proc === 'undefined' && expr.procedure.type === 'path' && environment.lookup(expr.procedure[0].value)) { - // help the user out here if they simply forgot the leading $ - throw { - message: 'Attempted to partially apply a non-function. Did you mean \'$' + expr.procedure[0].value + '\'?', - stack: (new Error()).stack, - position: expr.position, - token: expr.procedure[0].value - }; - } - if (proc && proc.lambda) { - result = partialApplyProcedure(proc, evaluatedArgs); - } else if (typeof proc === 'function') { - result = partialApplyNativeFunction(proc, evaluatedArgs); - } else { - throw { - message: 'Attempted to partially apply a non-function', - stack: (new Error()).stack, - position: expr.position, - token: expr.procedure.type === 'path' ? expr.procedure[0].value : expr.procedure.value - }; - } - return result; - } - - /** - * Apply procedure - * @param {Object} proc - Procedure - * @param {Array} args - Arguments - * @returns {*} Result of procedure - */ - function applyProcedure(proc, args) { - var result; - var env = createFrame(proc.environment); - proc.arguments.forEach(function (param, index) { - env.bind(param.value, args[index]); - }); - if (typeof proc.body === 'function') { - // this is a lambda that wraps a native function - generated by partially evaluating a native - result = applyNativeFunction(proc.body, env); - } else { - result = evaluate(proc.body, proc.input, env); - } - return result; - } - - /** - * Partially apply procedure - * @param {Object} proc - Procedure - * @param {Array} args - Arguments - * @returns {{lambda: boolean, input: *, environment: {bind, lookup}, arguments: Array, body: *}} Result of partially applied procedure - */ - function partialApplyProcedure(proc, args) { - // create a closure, bind the supplied parameters and return a function that takes the remaining (?) parameters - var env = createFrame(proc.environment); - var unboundArgs = []; - proc.arguments.forEach(function (param, index) { - var arg = args[index]; - if (arg && arg.type === 'operator' && arg.value === '?') { - unboundArgs.push(param); - } else { - env.bind(param.value, arg); - } - }); - var procedure = { - lambda: true, - input: proc.input, - environment: env, - arguments: unboundArgs, - body: proc.body - }; - return procedure; - } - - /** - * Partially apply native function - * @param {Function} native - Native function - * @param {Array} args - Arguments - * @returns {{lambda: boolean, input: *, environment: {bind, lookup}, arguments: Array, body: *}} Result of partially applying native function - */ - function partialApplyNativeFunction(native, args) { - // create a lambda function that wraps and invokes the native function - // get the list of declared arguments from the native function - // this has to be picked out from the toString() value - var sigArgs = getNativeFunctionArguments(native); - sigArgs = sigArgs.map(function (sigArg) { - return '$' + sigArg.trim(); - }); - var body = 'function(' + sigArgs.join(', ') + '){ _ }'; - - var bodyAST = parser(body); - bodyAST.body = native; - - var partial = partialApplyProcedure(bodyAST, args); - return partial; - } - - /** - * Apply native function - * @param {Object} proc - Procedure - * @param {Object} env - Environment - * @returns {*} Result of applying native function - */ - function applyNativeFunction(proc, env) { - var sigArgs = getNativeFunctionArguments(proc); - // generate the array of arguments for invoking the function - look them up in the environment - var args = sigArgs.map(function (sigArg) { - return env.lookup(sigArg.trim()); - }); - - var result = proc.apply(null, args); - return result; - } - - /** - * Get native function arguments - * @param {Function} func - Function - * @returns {*|Array} Native function arguments - */ - function getNativeFunctionArguments(func) { - var signature = func.toString(); - var sigParens = /\(([^\)]*)\)/.exec(signature)[1]; // the contents of the parens - var sigArgs = sigParens.split(','); - return sigArgs; - } - - /** - * Tests whether arg is a lambda function - * @param {*} arg - the value to test - * @returns {boolean} - true if it is a lambda function - */ - function isLambda(arg) { - var result = false; - if(arg && typeof arg === 'object' && - arg.lambda === true && - arg.hasOwnProperty('input') && - arg.hasOwnProperty('arguments') && - arg.hasOwnProperty('environment') && - arg.hasOwnProperty('body')) { - result = true; - } - - return result; - } - - /** - * Sum function - * @param {Object} args - Arguments - * @returns {number} Total value of arguments - */ - function functionSum(args) { - var total = 0; - - if (arguments.length != 1) { - throw { - message: 'The sum function expects one argument', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof args === 'undefined') { - return undefined; - } - - if(!Array.isArray(args)) { - args = [args]; - } - - // it must be an array of numbers - var nonNumerics = args.filter(function(val) {return (typeof val !== 'number');}); - if(nonNumerics.length > 0) { - throw { - message: 'Type error: argument of sum function must be an array of numbers', - stack: (new Error()).stack, - value: nonNumerics - }; - } - args.forEach(function(num){total += num;}); - return total; - } - - /** - * Count function - * @param {Object} args - Arguments - * @returns {number} Number of elements in the array - */ - function functionCount(args) { - if (arguments.length != 1) { - throw { - message: 'The count function expects one argument', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof args === 'undefined') { - return 0; - } - - if(!Array.isArray(args)) { - args = [args]; - } - - return args.length; - } - - /** - * Max function - * @param {Object} args - Arguments - * @returns {number} Max element in the array - */ - function functionMax(args) { - var max; - - if (arguments.length != 1) { - throw { - message: 'The max function expects one argument', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof args === 'undefined') { - return undefined; - } - - if(!Array.isArray(args)) { - args = [args]; - } - - // it must be an array of numbers - var nonNumerics = args.filter(function(val) {return (typeof val !== 'number');}); - if(nonNumerics.length > 0) { - throw { - message: 'Type error: argument of max function must be an array of numbers', - stack: (new Error()).stack, - value: nonNumerics - }; - } - max = Math.max.apply(Math, args); - return max; - } - - /** - * Min function - * @param {Object} args - Arguments - * @returns {number} Min element in the array - */ - function functionMin(args) { - var min; - - if (arguments.length != 1) { - throw { - message: 'The min function expects one argument', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof args === 'undefined') { - return undefined; - } - - if(!Array.isArray(args)) { - args = [args]; - } - - // it must be an array of numbers - var nonNumerics = args.filter(function(val) {return (typeof val !== 'number');}); - if(nonNumerics.length > 0) { - throw { - message: 'Type error: argument of min function must be an array of numbers', - stack: (new Error()).stack, - value: nonNumerics - }; - } - min = Math.min.apply(Math, args); - return min; - } - - /** - * Average function - * @param {Object} args - Arguments - * @returns {number} Average element in the array - */ - function functionAverage(args) { - var total = 0; - - if (arguments.length != 1) { - throw { - message: 'The average function expects one argument', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof args === 'undefined') { - return undefined; - } - - if(!Array.isArray(args)) { - args = [args]; - } - - // it must be an array of numbers - var nonNumerics = args.filter(function(val) {return (typeof val !== 'number');}); - if(nonNumerics.length > 0) { - throw { - message: 'Type error: argument of average function must be an array of numbers', - stack: (new Error()).stack, - value: nonNumerics - }; - } - args.forEach(function(num){total += num;}); - return total/args.length; - } - - /** - * Stingify arguments - * @param {Object} arg - Arguments - * @returns {String} String from arguments - */ - function functionString(arg) { - var str; - - if(arguments.length != 1) { - throw { - message: 'The string function expects one argument', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof arg === 'undefined') { - return undefined; - } - - if (typeof arg === 'string') { - // already a string - str = arg; - } else if(typeof arg === 'function' || isLambda(arg)) { - // functions (built-in and lambda convert to empty string - str = ''; - } else if (typeof arg === 'number' && !isFinite(arg)) { - throw { - message: "Attempting to invoke string function on Infinity or NaN", - value: arg, - stack: (new Error()).stack - }; - } else - str = JSON.stringify(arg, function (key, val) { - return (typeof val !== 'undefined' && val !== null && val.toPrecision && isNumeric(val)) ? Number(val.toPrecision(13)) : - (val && isLambda(val)) ? '' : - (typeof val === 'function') ? '' : val; - }); - return str; - } - - /** - * Create substring based on character number and length - * @param {String} str - String to evaluate - * @param {Integer} start - Character number to start substring - * @param {Integer} [length] - Number of characters in substring - * @returns {string|*} Substring - */ - function functionSubstring(str, start, length) { - if(arguments.length != 2 && arguments.length != 3) { - throw { - message: 'The substring function expects two or three arguments', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof str === 'undefined') { - return undefined; - } - - // otherwise it must be a string - if(typeof str !== 'string') { - throw { - message: 'Type error: first argument of substring function must evaluate to a string', - stack: (new Error()).stack, - value: str - }; - } - - if(typeof start !== 'number') { - throw { - message: 'Type error: second argument of substring function must evaluate to a number', - stack: (new Error()).stack, - value: start - }; - } - - if(typeof length !== 'undefined' && typeof length !== 'number') { - throw { - message: 'Type error: third argument of substring function must evaluate to a number', - stack: (new Error()).stack, - value: length - }; - } - - return str.substr(start, length); - } - - /** - * Create substring up until a character - * @param {String} str - String to evaluate - * @param {String} chars - Character to define substring boundary - * @returns {*} Substring - */ - function functionSubstringBefore(str, chars) { - if(arguments.length != 2) { - throw { - message: 'The substringBefore function expects two arguments', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof str === 'undefined') { - return undefined; - } - - // otherwise it must be a string - if(typeof str !== 'string') { - throw { - message: 'Type error: first argument of substringBefore function must evaluate to a string', - stack: (new Error()).stack, - value: str - }; - } - if(typeof chars !== 'string') { - throw { - message: 'Type error: second argument of substringBefore function must evaluate to a string', - stack: (new Error()).stack, - value: chars - }; - } - - var pos = str.indexOf(chars); - if (pos > -1) { - return str.substr(0, pos); - } else { - return str; - } - } - - /** - * Create substring after a character - * @param {String} str - String to evaluate - * @param {String} chars - Character to define substring boundary - * @returns {*} Substring - */ - function functionSubstringAfter(str, chars) { - if(arguments.length != 2) { - throw { - message: 'The substringAfter function expects two arguments', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof str === 'undefined') { - return undefined; - } - - // otherwise it must be a string - if(typeof str !== 'string') { - throw { - message: 'Type error: first argument of substringAfter function must evaluate to a string', - stack: (new Error()).stack, - value: str - }; - } - if(typeof chars !== 'string') { - throw { - message: 'Type error: second argument of substringAfter function must evaluate to a string', - stack: (new Error()).stack, - value: chars - }; - } - - var pos = str.indexOf(chars); - if (pos > -1) { - return str.substr(pos + chars.length); - } else { - return str; - } - } - - /** - * Lowercase a string - * @param {String} str - String to evaluate - * @returns {string} Lowercase string - */ - function functionLowercase(str) { - if(arguments.length != 1) { - throw { - message: 'The lowercase function expects one argument', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof str === 'undefined') { - return undefined; - } - - // otherwise it must be a string - if(typeof str !== 'string') { - throw { - message: 'Type error: argument of lowercase function must evaluate to a string', - stack: (new Error()).stack, - value: str - }; - } - - return str.toLowerCase(); - } - - /** - * Uppercase a string - * @param {String} str - String to evaluate - * @returns {string} Uppercase string - */ - function functionUppercase(str) { - if(arguments.length != 1) { - throw { - message: 'The uppercase function expects one argument', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof str === 'undefined') { - return undefined; - } - - // otherwise it must be a string - if(typeof str !== 'string') { - throw { - message: 'Type error: argument of uppercase function must evaluate to a string', - stack: (new Error()).stack, - value: str - }; - } - - return str.toUpperCase(); - } - - /** - * length of a string - * @param {String} str - string - * @returns {Number} The number of characters in the string - */ - function functionLength(str) { - if(arguments.length != 1) { - throw { - message: 'The length function expects one argument', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof str === 'undefined') { - return undefined; - } - - // otherwise it must be a string - if(typeof str !== 'string') { - throw { - message: 'Type error: argument of length function must evaluate to a string', - stack: (new Error()).stack, - value: str - }; - } - - return str.length; - } - - /** - * Split a string into an array of substrings - * @param {String} str - string - * @param {String} separator - the token that splits the string - * @param {Integer} [limit] - max number of substrings - * @returns {Array} The array of string - */ - function functionSplit(str, separator, limit) { - if(arguments.length != 2 && arguments.length != 3) { - throw { - message: 'The split function expects two or three arguments', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof str === 'undefined') { - return undefined; - } - - // otherwise it must be a string - if(typeof str !== 'string') { - throw { - message: 'Type error: first argument of split function must evaluate to a string', - stack: (new Error()).stack, - value: str - }; - } - - // separator must be a string - if(typeof separator !== 'string') { - throw { - message: 'Type error: second argument of split function must evaluate to a string', - stack: (new Error()).stack, - value: separator - }; - } - - // limit, if specified, must be a number - if(typeof limit !== 'undefined' && (typeof limit !== 'number' || limit < 0)) { - throw { - message: 'Type error: third argument of split function must evaluate to a positive number', - stack: (new Error()).stack, - value: limit - }; - } - - return str.split(separator, limit); - } - - /** - * Join an array of strings - * @param {Array} strs - array of string - * @param {String} [separator] - the token that splits the string - * @returns {String} The concatenated string - */ - function functionJoin(strs, separator) { - if(arguments.length != 1 && arguments.length != 2) { - throw { - message: 'The join function expects one or two arguments', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof strs === 'undefined') { - return undefined; - } - - if(!Array.isArray(strs)) { - strs = [strs]; - } - - // it must be an array of strings - var nonStrings = strs.filter(function(val) {return (typeof val !== 'string');}); - if(nonStrings.length > 0) { - throw { - message: 'Type error: first argument of join function must be an array of strings', - stack: (new Error()).stack, - value: nonStrings - }; - } - - - // if separator is not specified, default to empty string - if(typeof separator === 'undefined') { - separator = ""; - } - - // separator, if specified, must be a string - if(typeof separator !== 'string') { - throw { - message: 'Type error: second argument of split function must evaluate to a string', - stack: (new Error()).stack, - value: separator - }; - } - - return strs.join(separator); - } - - /** - * Cast argument to number - * @param {Object} arg - Argument - * @returns {Number} numeric value of argument - */ - function functionNumber(arg) { - var result; - - if(arguments.length != 1) { - throw { - message: 'The number function expects one argument', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof arg === 'undefined') { - return undefined; - } - - if (typeof arg === 'number') { - // already a number - result = arg; - } else if(typeof arg === 'string' && /^-?(0|([1-9][0-9]*))(\.[0-9]+)?([Ee][-+]?[0-9]+)?$/.test(arg) && !isNaN(parseFloat(arg)) && isFinite(arg)) { - result = parseFloat(arg); - } else { - throw { - message: "Unable to cast value to a number", - value: arg, - stack: (new Error()).stack - }; - } - return result; - } - - - /** - * Evaluate an input and return a boolean - * @param {*} arg - Arguments - * @returns {boolean} Boolean - */ - function functionBoolean(arg) { - // cast arg to its effective boolean value - // boolean: unchanged - // string: zero-length -> false; otherwise -> true - // number: 0 -> false; otherwise -> true - // null -> false - // array: empty -> false; length > 1 -> true - // object: empty -> false; non-empty -> true - // function -> false - - if(arguments.length != 1) { - throw { - message: 'The boolean function expects one argument', - stack: (new Error()).stack - }; - } - - // undefined inputs always return undefined - if(typeof arg === 'undefined') { - return undefined; - } - - var result = false; - if (Array.isArray(arg)) { - if (arg.length == 1) { - result = functionBoolean(arg[0]); - } else if (arg.length > 1) { - var trues = arg.filter(function(val) {return functionBoolean(val);}); - result = trues.length > 0; - } - } else if (typeof arg === 'string') { - if (arg.length > 0) { - result = true; - } - } else if (isNumeric(arg)) { - if (arg != 0) { - result = true; - } - } else if (arg != null && typeof arg === 'object') { - if (Object.keys(arg).length > 0) { - // make sure it's not a lambda function - if (!(isLambda(arg))) { - result = true; - } - } - } else if (typeof arg === 'boolean' && arg == true) { - result = true; - } - return result; - } - - /** - * returns the Boolean NOT of the arg - * @param {*} arg - argument - * @returns {boolean} - NOT arg - */ - function functionNot(arg) { - return !functionBoolean(arg); - } - - /** - * Create a map from an array of arguments - * @param {Function} func - function to apply - * @returns {Array} Map array - */ - function functionMap(func) { - // this can take a variable number of arguments - each one should be mapped to the equivalent arg of func - // assert that func is a function - var varargs = arguments; - var result = []; - - // each subsequent arg must be an array - coerce if not - var args = []; - for (var ii = 1; ii < varargs.length; ii++) { - if (Array.isArray(varargs[ii])) { - args.push(varargs[ii]); - } else { - args.push([varargs[ii]]); - } - - } - // do the map - iterate over the arrays, and invoke func - if (args.length > 0) { - for (var i = 0; i < args[0].length; i++) { - var func_args = []; - for (var j = 0; j < func.arguments.length; j++) { - func_args.push(args[j][i]); - } - // invoke func - result.push(apply(func, func_args, null, null)); - } - } - return result; - } - - /** - * Fold left function - * @param {Function} func - Function - * @param {Array} sequence - Sequence - * @param {Object} init - Initial value - * @returns {*} Result - */ - function functionFoldLeft(func, sequence, init) { - var result; - - if (!(func.length == 2 || func.arguments.length == 2)) { - throw { - message: 'The first argument of the reduce function must be a function of arity 2', - stack: (new Error()).stack - }; - } - - if (!Array.isArray(sequence)) { - sequence = [sequence]; - } - - var index; - if (typeof init === 'undefined' && sequence.length > 0) { - result = sequence[0]; - index = 1; - } else { - result = init; - index = 0; - } - - while (index < sequence.length) { - result = apply(func, [result, sequence[index]], null, null); - index++; - } - - return result; - } - - /** - * Return keys for an object - * @param {Object} arg - Object - * @returns {Array} Array of keys - */ - function functionKeys(arg) { - var result = []; - - if(Array.isArray(arg)) { - // merge the keys of all of the items in the array - var merge = {}; - arg.forEach(function(item) { - var keys = functionKeys(item); - if(Array.isArray(keys)) { - keys.forEach(function(key) { - merge[key] = true; - }); - } - }); - result = functionKeys(merge); - } else if(arg != null && typeof arg === 'object' && !(isLambda(arg))) { - result = Object.keys(arg); - if(result.length == 0) { - result = undefined; - } - } else { - result = undefined; - } - return result; - } - - /** - * Return value from an object for a given key - * @param {Object} object - Object - * @param {String} key - Key in object - * @returns {*} Value of key in object - */ - function functionLookup(object, key) { - var result = evaluateName({value: key}, object); - return result; - } - - /** - * Append second argument to first - * @param {Array|Object} arg1 - First argument - * @param {Array|Object} arg2 - Second argument - * @returns {*} Appended arguments - */ - function functionAppend(arg1, arg2) { - // disregard undefined args - if (typeof arg1 === 'undefined') { - return arg2; - } - if (typeof arg2 === 'undefined') { - return arg1; - } - // if either argument is not an array, make it so - if (!Array.isArray(arg1)) { - arg1 = [arg1]; - } - if (!Array.isArray(arg2)) { - arg2 = [arg2]; - } - Array.prototype.push.apply(arg1, arg2); - return arg1; - } - - /** - * Determines if the argument is undefined - * @param {*} arg - argument - * @returns {boolean} False if argument undefined, otherwise true - */ - function functionExists(arg){ - if (arguments.length != 1) { - throw { - message: 'The exists function expects one argument', - stack: (new Error()).stack - }; - } - - if (typeof arg === 'undefined') { - return false; - } else { - return true; - } - } - - /** - * Splits an object into an array of object with one property each - * @param {*} arg - the object to split - * @returns {*} - the array - */ - function functionSpread(arg) { - var result = []; - - if(Array.isArray(arg)) { - // spread all of the items in the array - arg.forEach(function(item) { - result = functionAppend(result, functionSpread(item)); - }); - } else if(arg != null && typeof arg === 'object' && !isLambda(arg)) { - for(var key in arg) { - var obj = {}; - obj[key] = arg[key]; - result.push(obj); - } - } else { - result = arg; - } - return result; - } - - /** - * Create frame - * @param {Object} enclosingEnvironment - Enclosing environment - * @returns {{bind: bind, lookup: lookup}} Created frame - */ - function createFrame(enclosingEnvironment) { - var bindings = {}; - return { - bind: function (name, value) { - bindings[name] = value; - }, - lookup: function (name) { - var value = bindings[name]; - if (typeof value === 'undefined' && enclosingEnvironment) { - value = enclosingEnvironment.lookup(name); - } - return value; - } - }; - } - - var staticFrame = createFrame(null); - - staticFrame.bind('sum', functionSum); - staticFrame.bind('count', functionCount); - staticFrame.bind('max', functionMax); - staticFrame.bind('min', functionMin); - staticFrame.bind('average', functionAverage); - staticFrame.bind('string', functionString); - staticFrame.bind('substring', functionSubstring); - staticFrame.bind('substringBefore', functionSubstringBefore); - staticFrame.bind('substringAfter', functionSubstringAfter); - staticFrame.bind('lowercase', functionLowercase); - staticFrame.bind('uppercase', functionUppercase); - staticFrame.bind('length', functionLength); - staticFrame.bind('split', functionSplit); - staticFrame.bind('join', functionJoin); - staticFrame.bind('number', functionNumber); - staticFrame.bind('boolean', functionBoolean); - staticFrame.bind('not', functionNot); - staticFrame.bind('map', functionMap); - staticFrame.bind('reduce', functionFoldLeft); - staticFrame.bind('keys', functionKeys); - staticFrame.bind('lookup', functionLookup); - staticFrame.bind('append', functionAppend); - staticFrame.bind('exists', functionExists); - staticFrame.bind('spread', functionSpread); - - /** - * JSONata - * @param {Object} expr - JSONata expression - * @returns {{evaluate: evaluate, assign: assign}} Evaluated expression - */ - function jsonata(expr) { - var ast = parser(expr); - var environment = createFrame(staticFrame); - return { - evaluate: function (input, bindings) { - if (typeof bindings !== 'undefined') { - var exec_env; - // the variable bindings have been passed in - create a frame to hold these - exec_env = createFrame(environment); - for (var v in bindings) { - exec_env.bind(v, bindings[v]); - } - } else { - exec_env = environment; - } - // put the input document into the environment as the root object - exec_env.bind('$', input); - return evaluate(ast, input, exec_env); - }, - assign: function (name, value) { - environment.bind(name, value); - } - }; - } - - jsonata.parser = parser; - - return jsonata; - -})(); - -// node.js only - export the jsonata and parser functions -// istanbul ignore else -if(typeof module !== 'undefined') { - module.exports = jsonata; -} diff --git a/editor/vendor/jsonata/mode-jsonata.js b/editor/vendor/jsonata/mode-jsonata.js index 50190e7b3..fb5883db7 100644 --- a/editor/vendor/jsonata/mode-jsonata.js +++ b/editor/vendor/jsonata/mode-jsonata.js @@ -6,6 +6,13 @@ define("ace/mode/jsonata",["require","exports","module","ace/lib/oop","ace/mode/ var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; var WorkerClient = require("../worker/worker_client").WorkerClient; + var jsonataFunctions = Object.keys(jsonata.functions); + // sort in length order (long->short) otherwise substringAfter gets matched + // as substring etc. + jsonataFunctions.sort(function(A,B) { + return B.length-A.length; + }); + jsonataFunctions = jsonataFunctions.join("|").replace(/\$/g,"\\$"); var JSONataHighlightRules = function() { @@ -17,11 +24,7 @@ define("ace/mode/jsonata",["require","exports","module","ace/lib/oop","ace/mode/ "constant.language.boolean": "true|false", "storage.type": - "function", - "keyword": - "$sum|$count|$max|$min|$average|$string|$substring|$substringBefore|"+ - "$substringAfter|$lowercase|$uppercase|$length|$split|$join|$number|"+ - "$boolean|$not|$map|$reduce|$keys|$lookup|$append|$exists|$spread" + "function" }, "identifier"); this.$rules = { "start" : [ @@ -46,6 +49,10 @@ define("ace/mode/jsonata",["require","exports","module","ace/lib/oop","ace/mode/ { token: "keyword", regex: /λ/ }, + { + token: "keyword", + regex: jsonataFunctions + }, { token : keywordMapper, regex : "[a-zA-Z\\$_\u00a1-\uffff][a-zA-Z\\d\\$_\u00a1-\uffff]*"