From bf90509526a66b295e4c438dc5c88594563707c4 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 10 Nov 2016 23:58:34 +0000 Subject: [PATCH 1/8] Add jsonata support to Change/Switch nodes --- Gruntfile.js | 3 +- editor/images/typedInput/expr.png | Bin 0 -> 786 bytes editor/js/ui/common/typedInput.js | 3 +- editor/templates/index.mst | 1 + editor/vendor/jsonata/jsonata.js | 2805 ++++++++++++++++++++++++ nodes/core/locales/en-US/messages.json | 3 + nodes/core/logic/10-switch.html | 8 +- nodes/core/logic/10-switch.js | 53 +- nodes/core/logic/15-change.html | 2 +- nodes/core/logic/15-change.js | 10 + package.json | 1 + red/runtime/util.js | 3 + 12 files changed, 2884 insertions(+), 8 deletions(-) create mode 100644 editor/images/typedInput/expr.png create mode 100644 editor/vendor/jsonata/jsonata.js diff --git a/Gruntfile.js b/Gruntfile.js index ffb1df3c3..4fc9b6fed 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -160,7 +160,8 @@ module.exports = function(grunt) { build: { files: { 'public/red/red.min.js': 'public/red/red.js', - 'public/red/main.min.js': 'public/red/main.js' + 'public/red/main.min.js': 'public/red/main.js', + 'public/vendor/jsonata/jsonata.min.js': 'editor/vendor/jsonata/jsonata.js' } } }, diff --git a/editor/images/typedInput/expr.png b/editor/images/typedInput/expr.png new file mode 100644 index 0000000000000000000000000000000000000000..74d9516aee3c9d64b8f0455bdab7f82b94208433 GIT binary patch literal 786 zcmV+t1MU2YP)53WFU8GbZ8()Nlj2>E@cM*00MSNL_t(o!|j(#YZFlv z$N%RhnMRZ##rRruV?c=5g<+UUf)GVSC=?e8tybE83BQ9MLTQ2(H@fO$B@IYsh+#m{ zg<^1F7fOsSL@}8(bKFF@xhCl(ooq_Z@^J3`o%fvqCNhyBh5Q(0GMW1T9!RPemStTV z1t|O&HJi->Gw(^NE9rE4W^6#malQiBl+Q8q^4Nd?AcT0>RSTkJza<$77>Pu-nE8tY zJZGBb<*@;a#bTR?@=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&1Uno<2XML93e9=NiZ<;hHcv) zn0ZSwJECdYLLezs((s6=>`d+pG)+?{q6-qd)@U@g0iaf^Z2eyO+!5LL&j=@%TJ|X(^qr zEz9Z}hjO|63c!2Gp|h^*ULOhAdDZ3S$@ek!-8_APx~`vM<{R>JD1=z=>^{tm}sZ`npup>Dz6$*tGhXVFU=h~q3d!ASD^ZC&eXc)#N z0JDHet@=Z?=bUcDXk|pO`Dti!W}O83&4u%I{^j^ Qu>b%707*qoM6N<$f?82bZU6uP literal 0 HcmV?d00001 diff --git a/editor/js/ui/common/typedInput.js b/editor/js/ui/common/typedInput.js index a455efc66..b30af8961 100644 --- a/editor/js/ui/common/typedInput.js +++ b/editor/js/ui/common/typedInput.js @@ -94,7 +94,8 @@ bool: {value:"bool",label:"boolean",icon:"red/images/typedInput/bool.png",options:["true","false"]}, json: {value:"json",label:"JSON",icon:"red/images/typedInput/json.png", validate: function(v) { try{JSON.parse(v);return true;}catch(e){return false;}}}, re: {value:"re",label:"regular expression",icon:"red/images/typedInput/re.png"}, - date: {value:"date",label:"timestamp",hasValue:false} + date: {value:"date",label:"timestamp",hasValue:false}, + jsonata: {value:"jsonata",label:"expression",icon:"red/images/typedInput/expr.png", validate: function(v) { try{jsonata(v);return true;}catch(e){return false;}}}, }; var nlsd = false; diff --git a/editor/templates/index.mst b/editor/templates/index.mst index 146cc866b..0cd340863 100644 --- a/editor/templates/index.mst +++ b/editor/templates/index.mst @@ -160,6 +160,7 @@ + diff --git a/editor/vendor/jsonata/jsonata.js b/editor/vendor/jsonata/jsonata.js new file mode 100644 index 000000000..2883b5fc4 --- /dev/null +++ b/editor/vendor/jsonata/jsonata.js @@ -0,0 +1,2805 @@ +/** + * © Copyright IBM Corp. 2016 All Rights Reserved + * Project name: JSONata + * This project is licensed under the MIT License, see LICENSE + */ + +'use strict'; +/** + * @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() { + 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 at column " + position, + stack: (new Error()).stack, + position: position + }; + } + } else { + // illegal escape sequence + throw { + message: 'unsupported escape sequence: \\' + currentChar + ' at column ' + position, + 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 at column ' + position, + 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] + ' at column ' + position, + 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 + "' at column " + node.position; + } + 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 + " at column " + next_token.position, + 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 + " at column " + next_token.position, + 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; + }); + + // object constructor + prefix("{", function () { + 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("}"); + this.lhs = a; + this.type = "unary"; + 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) { + this.lhs = left; + this.rhs = expression(operators[']']); + this.type = 'binary'; + advance("]"); + return this; + }); + + // aggregator + infix("{", operators['{'], function (left) { + this.lhs = left; + this.rhs = expression(operators['}']); + this.type = 'binary'; + advance("}"); + return this; + }); + + // 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 step = post_parse(expr.lhs); + if (Array.isArray(step)) { + Array.prototype.push.apply(result, step); + } else { + result.push(step); + } + var rest = [post_parse(expr.rhs)]; + 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); + if (typeof result.aggregate !== 'undefined') { + throw { + message: 'A predicate cannot follow an aggregate in a step. Error at column: ' + expr.position, + stack: (new Error()).stack, + position: expr.position + }; + } + if (typeof result.predicate === 'undefined') { + result.predicate = []; + } + result.predicate.push(post_parse(expr.rhs)); + break; + case '{': + // aggregate + // LHS is a step or a predicated step + // RHS is the predicate expr + result = post_parse(expr.lhs); + if (typeof result.aggregate !== 'undefined') { + throw { + message: 'Each step can only have one aggregator. Error at column: ' + expr.position, + stack: (new Error()).stack, + position: expr.position + }; + } + result.aggregate = post_parse(expr.rhs); + 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': + 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 + " at column " + expr.position, + stack: (new Error()).stack, + position: expr.position, + token: expr.value + }; + } + break; + default: + var reason = "Unknown expression type: " + expr.value + " at column " + expr.position; + /* 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 + " at column " + node.position, + stack: (new Error()).stack, + position: node.position, + token: node.value + }; + } + expr = post_parse(expr); + + // a single name token is a single step location path + if (expr.type === 'name') { + expr = [expr]; + expr.type = 'path'; + } + + 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('aggregate')) { + result = applyAggregate(expr.aggregate, 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; + // 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 + expr.forEach(function (step) { + 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'; + } + if (step.value === '{') { + if(typeof input !== 'undefined') { + result = evaluateGroupExpression(step, inputSequence, environment); + } + } else { + inputSequence.forEach(function (item) { + var res = evaluate(step, item, environment); + if (typeof res !== 'undefined') { + if (Array.isArray(res)) { + // 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) { + result = resultSequence[0]; + } else if (resultSequence.length > 1) { + result = resultSequence; + } + } + + 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; + } + + /** + * Apply aggregate to input data + * @param {Object} expr - JSONata expression + * @param {Object} input - Input data to evaluate against + * @param {Object} environment - Environment + * @returns {{}} Result after applying aggregate + */ + function applyAggregate(expr, input, environment) { + var result = {}; + // this is effectively a 'reduce' HOF (fold left) + // if the input is a singleton, then just return this as the result + // otherwise iterate over the input array and aggregate the result + if (Array.isArray(input)) { + // create a new frame to limit the scope + var aggEnv = createFrame(environment); + // the variable $@ will hold the aggregated value, initialize this to the first array item + aggEnv.bind('_', input[0]); + // loop over the remainder of the array + for (var index = 1; index < input.length; index++) { + var reduce = evaluate(expr, input[index], aggEnv); + aggEnv.bind('_', reduce); + } + result = aggEnv.lookup('_'); + } else { + result = input; + } + 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 + " at column " + expr.position, + 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 === '..') { + // array generated by the range operator - merge into results + result = functionAppend(result, value); + } else { + result.push(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 at column ' + expr.position, + 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 at column ' + expr.position, + 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 + ' at column ' + expr.position, + 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 at column ' + expr.position, + 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 at column ' + expr.position, + 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 $) at column " + expr.position, + stack: (new Error()).stack, + position: expr.position, + token: expr.value, + 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 === 'name' && environment.lookup(expr.procedure.value)) { + // help the user out here if they simply forgot the leading $ + throw { + message: 'Attempted to invoke a non-function at column ' + expr.position + '. Did you mean \'$' + expr.procedure.value + '\'?', + stack: (new Error()).stack, + position: expr.position, + token: expr.procedure.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.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 === 'name' && environment.lookup(expr.procedure.value)) { + // help the user out here if they simply forgot the leading $ + throw { + message: 'Attempted to partially apply a non-function at column ' + expr.position + '. Did you mean \'$' + expr.procedure.value + '\'?', + stack: (new Error()).stack, + position: expr.position, + token: expr.procedure.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 at column ' + expr.position, + stack: (new Error()).stack, + position: expr.position, + token: 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/nodes/core/locales/en-US/messages.json b/nodes/core/locales/en-US/messages.json index ab733c45d..ee8a20264 100644 --- a/nodes/core/locales/en-US/messages.json +++ b/nodes/core/locales/en-US/messages.json @@ -502,6 +502,9 @@ "null":"is null", "nnull":"is not null", "else":"otherwise" + }, + "errors": { + "invalid-expr": "Invalid expression: __error__" } }, "change": { diff --git a/nodes/core/logic/10-switch.html b/nodes/core/logic/10-switch.html index 1330df14d..3ce53c89f 100644 --- a/nodes/core/logic/10-switch.html +++ b/nodes/core/logic/10-switch.html @@ -64,7 +64,7 @@ var node = this; var previousValueType = {value:"prev",label:this._("inject.previous"),hasValue:false}; - $("#node-input-property").typedInput({default:this.propertyType||'msg',types:['msg','flow','global']}); + $("#node-input-property").typedInput({default:this.propertyType||'msg',types:['msg','flow','global','jsonata']}); var operators = [ {v:"eq",t:"=="}, {v:"neq",t:"!="}, @@ -129,10 +129,10 @@ for (var d in operators) { selectField.append($("").val(operators[d].v).text(operators[d].t)); } - var valueField = $('',{class:"node-input-rule-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'str',types:['msg','flow','global','str','num',previousValueType]}); - var btwnValueField = $('',{class:"node-input-rule-btwn-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'num',types:['msg','flow','global','str','num',previousValueType]}); + var valueField = $('',{class:"node-input-rule-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'str',types:['msg','flow','global','str','num','jsonata',previousValueType]}); + var btwnValueField = $('',{class:"node-input-rule-btwn-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'num',types:['msg','flow','global','str','num','jsonata',previousValueType]}); var btwnAndLabel = $('',{class:"node-input-rule-btwn-label"}).text(" "+andLabel+" ").appendTo(row3); - var btwnValue2Field = $('',{class:"node-input-rule-btwn-value2",type:"text",style:"margin-left:2px;"}).appendTo(row3).typedInput({default:'num',types:['msg','flow','global','str','num',previousValueType]}); + var btwnValue2Field = $('',{class:"node-input-rule-btwn-value2",type:"text",style:"margin-left:2px;"}).appendTo(row3).typedInput({default:'num',types:['msg','flow','global','str','num','jsonata',previousValueType]}); var finalspan = $('',{style:"float: right;margin-top: 6px;"}).appendTo(row); finalspan.append(' → '+(i+1)+' '); var caseSensitive = $('',{id:"node-input-rule-case-"+i,class:"node-input-rule-case",type:"checkbox",style:"width:auto;vertical-align:top"}).appendTo(row2); diff --git a/nodes/core/logic/10-switch.js b/nodes/core/logic/10-switch.js index 6585eace1..3a335a675 100644 --- a/nodes/core/logic/10-switch.js +++ b/nodes/core/logic/10-switch.js @@ -16,6 +16,9 @@ module.exports = function(RED) { "use strict"; + + var jsonata = require('jsonata'); + var operators = { 'eq': function(a, b) { return a == b; }, 'neq': function(a, b) { return a != b; }, @@ -38,9 +41,20 @@ module.exports = function(RED) { this.rules = n.rules || []; this.property = n.property; this.propertyType = n.propertyType || "msg"; + + if (this.propertyType === 'jsonata') { + try { + this.property = jsonata(this.property); + } catch(err) { + this.error(RED._("switch.errors.invalid-expr",{error:err.message})); + return; + } + } + this.checkall = n.checkall || "true"; this.previousValue = null; var node = this; + var valid = true; for (var i=0; i',{class:"node-input-rule-property-value",type:"text"}) .appendTo(row2) - .typedInput({default:'str',types:['msg','flow','global','str','num','bool','json','date']}); + .typedInput({default:'str',types:['msg','flow','global','str','num','bool','json','date','jsonata']}); var row3_1 = $('
').appendTo(row3); $('
',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"}) diff --git a/nodes/core/logic/15-change.js b/nodes/core/logic/15-change.js index 67f30f12f..805b708ed 100644 --- a/nodes/core/logic/15-change.js +++ b/nodes/core/logic/15-change.js @@ -16,6 +16,7 @@ module.exports = function(RED) { "use strict"; + var jsonata = require("jsonata"); function ChangeNode(n) { RED.nodes.createNode(this, n); @@ -85,6 +86,13 @@ module.exports = function(RED) { } } else if (rule.tot === 'bool') { rule.to = /^true$/i.test(rule.to); + } else if (rule.tot === 'jsonata') { + try { + rule.to = jsonata(rule.to); + } catch(e) { + valid = false; + this.error(RED._("change.errors.invalid-from",{error:e.message})); + } } } @@ -107,6 +115,8 @@ module.exports = function(RED) { value = node.context().global.get(rule.to); } else if (rule.tot === 'date') { value = Date.now(); + } else if (rule.tot === 'jsonata') { + value = rule.to.evaluate({msg:msg}); } if (rule.t === 'change') { if (rule.fromt === 'msg' || rule.fromt === 'flow' || rule.fromt === 'global') { diff --git a/package.json b/package.json index ac20ba7e0..33de70e92 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "fs.notify":"0.0.4", "i18next":"1.10.6", "is-utf8":"0.2.1", + "jsonata":"1.0.7", "media-typer": "0.3.0", "mqtt": "1.14.1", "mustache": "2.2.1", diff --git a/red/runtime/util.js b/red/runtime/util.js index f132564c8..97213a389 100644 --- a/red/runtime/util.js +++ b/red/runtime/util.js @@ -15,6 +15,7 @@ **/ var clone = require("clone"); +var jsonata = require("jsonata"); function generateId() { return (1+Math.random()*4294967295).toString(16); @@ -310,6 +311,8 @@ function evaluateNodeProperty(value, type, node, msg) { return node.context().global.get(value); } else if (type === 'bool') { return /^true$/i.test(value); + } else if (type === 'jsonata') { + return jsonata(value).evaluate({msg:msg}); } return value; } From d33029027f3dcddc3271d91ec0bb574dd12b520d Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 15 Nov 2016 00:19:04 +0000 Subject: [PATCH 2/8] Add expression editor for jsonata --- Gruntfile.js | 4 +- editor/js/ui/common/typedInput.js | 40 +- editor/js/ui/editor.js | 170 +- editor/sass/ace.scss | 8 + editor/sass/style.scss | 1 + editor/sass/ui/common/typedInput.scss | 16 + editor/templates/index.mst | 7 + editor/vendor/jsonata/mode-jsonata.js | 129 + editor/vendor/jsonata/worker-jsonata.js | 4233 +++++++++++++++++++++++ 9 files changed, 4540 insertions(+), 68 deletions(-) create mode 100644 editor/sass/ace.scss create mode 100644 editor/vendor/jsonata/mode-jsonata.js create mode 100644 editor/vendor/jsonata/worker-jsonata.js diff --git a/Gruntfile.js b/Gruntfile.js index 4fc9b6fed..0e15c8dcd 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -161,7 +161,9 @@ module.exports = function(grunt) { files: { 'public/red/red.min.js': 'public/red/red.js', 'public/red/main.min.js': 'public/red/main.js', - 'public/vendor/jsonata/jsonata.min.js': 'editor/vendor/jsonata/jsonata.js' + 'public/vendor/jsonata/jsonata.min.js': 'editor/vendor/jsonata/jsonata.js', + 'public/vendor/ace/mode-jsonata.js': 'editor/vendor/jsonata/mode-jsonata.js', + 'public/vendor/ace/worker-jsonata.js': 'editor/vendor/jsonata/worker-jsonata.js' } } }, diff --git a/editor/js/ui/common/typedInput.js b/editor/js/ui/common/typedInput.js index b30af8961..c433c0366 100644 --- a/editor/js/ui/common/typedInput.js +++ b/editor/js/ui/common/typedInput.js @@ -95,7 +95,21 @@ json: {value:"json",label:"JSON",icon:"red/images/typedInput/json.png", validate: function(v) { try{JSON.parse(v);return true;}catch(e){return false;}}}, re: {value:"re",label:"regular expression",icon:"red/images/typedInput/re.png"}, date: {value:"date",label:"timestamp",hasValue:false}, - jsonata: {value:"jsonata",label:"expression",icon:"red/images/typedInput/expr.png", validate: function(v) { try{jsonata(v);return true;}catch(e){return false;}}}, + jsonata: { + value: "jsonata", + label: "expression", + icon: "red/images/typedInput/expr.png", + validate: function(v) { try{jsonata(v);return true;}catch(e){return false;}}, + expand:function() { + var that = this; + RED.editor.editExpression({ + value: this.value(), + complete: function(v) { + that.value(v); + } + }) + } + } }; var nlsd = false; @@ -189,7 +203,7 @@ that.uiSelect.addClass('red-ui-typedInput-focus'); }); - + this.optionExpandButton = $('').appendTo(this.uiSelect); this.type(this.options.default||this.typeList[0].value); @@ -323,11 +337,16 @@ this.uiSelect.width(this.uiWidth); } if (this.typeMap[this.propertyType] && this.typeMap[this.propertyType].hasValue === false) { - this.selectTrigger.css('width',"100%"); + this.selectTrigger.addClass("red-ui-typedInput-full-width"); } else { - this.selectTrigger.width('auto'); + this.selectTrigger.removeClass("red-ui-typedInput-full-width"); var labelWidth = this._getLabelWidth(this.selectTrigger); this.elementDiv.css('left',labelWidth+"px"); + if (this.optionExpandButton.is(":visible")) { + this.elementDiv.css('right',"22px"); + } else { + this.elementDiv.css('right','0'); + } if (this.optionSelectTrigger) { this.optionSelectTrigger.css({'left':(labelWidth)+"px",'width':'calc( 100% - '+labelWidth+'px )'}); } @@ -397,6 +416,9 @@ this.selectLabel.text(opt.label); } if (opt.options) { + if (this.optionExpandButton) { + this.optionExpandButton.hide(); + } if (this.optionSelectTrigger) { this.optionSelectTrigger.show(); this.elementDiv.hide(); @@ -430,6 +452,16 @@ } this.elementDiv.show(); } + if (opt.expand && typeof opt.expand === 'function') { + this.optionExpandButton.show(); + this.optionExpandButton.off('click'); + this.optionExpandButton.on('click',function(evt) { + evt.preventDefault(); + opt.expand.call(that); + }) + } else { + this.optionExpandButton.hide(); + } this.element.trigger('change',this.propertyType,this.value()); } if (image) { diff --git a/editor/js/ui/editor.js b/editor/js/ui/editor.js index c689352b0..cb821d731 100644 --- a/editor/js/ui/editor.js +++ b/editor/js/ui/editor.js @@ -494,12 +494,13 @@ RED.editor = (function() { } function getEditStackTitle() { - var title = '
    '; for (var i=0;i').appendTo(trayBody); + dialogForm.html($("script[data-template-name='"+type+"']").html()); + ns = ns||"node-red"; + dialogForm.find('[data-i18n]').each(function() { + var current = $(this).attr("data-i18n"); + var keys = current.split(";"); + for (var i=0;i').prependTo(dialogForm); + dialogForm.submit(function(e) { e.preventDefault();}); + return dialogForm; + } + function showEditDialog(node) { var editing_node = node; editStack.push(node); @@ -763,33 +791,13 @@ RED.editor = (function() { if (editing_node) { RED.sidebar.info.refresh(editing_node); } - var trayBody = tray.find('.editor-tray-body'); - var dialogForm = $('
    ').appendTo(trayBody); - dialogForm.html($("script[data-template-name='"+type+"']").html()); var ns; if (node._def.set.module === "node-red") { ns = "node-red"; } else { ns = node._def.set.id; } - dialogForm.find('[data-i18n]').each(function() { - var current = $(this).attr("data-i18n"); - var keys = current.split(";"); - for (var i=0;i').prependTo(dialogForm); + var dialogForm = buildEditForm(tray,"dialog-form",type,ns); prepareEditDialog(node,node._def,"node-input"); dialogForm.i18n(); }, @@ -882,7 +890,6 @@ RED.editor = (function() { }, open: function(tray) { var trayHeader = tray.find(".editor-tray-header"); - var trayBody = tray.find(".editor-tray-body"); var trayFooter = tray.find(".editor-tray-footer"); if (node_def.hasUsers !== false) { @@ -890,21 +897,8 @@ RED.editor = (function() { } trayFooter.append(''); - var dialogForm = $('
    ').appendTo(trayBody); - dialogForm.html($("script[data-template-name='"+type+"']").html()); - dialogForm.find('[data-i18n]').each(function() { - var current = $(this).attr("data-i18n"); - if (current.indexOf(":") === -1) { - var prefix = ""; - if (current.indexOf("[")===0) { - var parts = current.split("]"); - prefix = parts[0]+"]"; - current = parts[1]; - } - $(this).attr("data-i18n",prefix+ns+":"+current); - } - }); - $('').prependTo(dialogForm); + var dialogForm = buildEditForm(tray,"node-config-dialog-edit-form",type,ns); + prepareEditDialog(editing_config_node,node_def,"node-config-input"); if (editing_config_node._def.exclusive) { $("#node-config-dialog-scope").hide(); @@ -1338,30 +1332,7 @@ RED.editor = (function() { if (editing_node) { RED.sidebar.info.refresh(editing_node); } - var trayBody = tray.find('.editor-tray-body'); - var dialogForm = $('
    ').appendTo(trayBody); - dialogForm.html($("script[data-template-name='subflow-template']").html()); - var ns = "node-red"; - dialogForm.find('[data-i18n]').each(function() { - var current = $(this).attr("data-i18n"); - var keys = current.split(";"); - for (var i=0;i').prependTo(dialogForm); - - dialogForm.submit(function(e) { e.preventDefault();}); + var dialogForm = buildEditForm(tray,"dialog-form","subflow-template"); subflowEditor = RED.editor.createEditor({ id: 'subflow-input-info-editor', mode: 'ace/mode/markdown', @@ -1397,6 +1368,78 @@ RED.editor = (function() { RED.tray.show(trayOptions); } + + function editExpression(options) { + var value = options.value; + var onComplete = options.complete; + var type = "_expression" + editStack.push({type:type}); + RED.view.state(RED.state.EDITING); + var expressionEditor; + + var trayOptions = { + title: getEditStackTitle(), + buttons: [ + { + id: "node-dialog-cancel", + text: RED._("common.label.cancel"), + click: function() { + RED.tray.close(); + } + }, + { + id: "node-dialog-ok", + text: RED._("common.label.done"), + class: "primary", + click: function() { + onComplete(expressionEditor.getValue()); + RED.tray.close(); + } + } + ], + resize: function(dimensions) { + editTrayWidthCache[type] = dimensions.width; + + var rows = $("#dialog-form>div:not(.node-text-editor-row)"); + var editorRow = $("#dialog-form>div.node-text-editor-row"); + var height = $("#dialog-form").height(); + for (var i=0;i
+ + + diff --git a/editor/vendor/jsonata/mode-jsonata.js b/editor/vendor/jsonata/mode-jsonata.js new file mode 100644 index 000000000..24562c536 --- /dev/null +++ b/editor/vendor/jsonata/mode-jsonata.js @@ -0,0 +1,129 @@ +define("ace/mode/jsonata",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules","ace/worker/worker_client","ace/mode/text"], function(require, exports, module) { + "use strict"; + + var oop = require("../lib/oop"); + var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; + var WorkerClient = require("../worker/worker_client").WorkerClient; + + var JSONataHighlightRules = function() { + + var keywordMapper = this.createKeywordMapper({ + "keyword.operator": + "and|or|in", + "constant.language": + "null|Infinity|NaN|undefined", + "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" + }, "identifier"); + this.$rules = { + "start" : [ + { + token : "string", + regex : "'(?=.)", + next : "qstring" + }, + { + token : "string", + regex : '"(?=.)', + next : "qqstring" + }, + { + token : "constant.numeric", // hex + regex : /0(?:[xX][0-9a-fA-F]+|[bB][01]+)\b/ + }, + { + token : "constant.numeric", // float + regex : /[+-]?\d[\d_]*(?:(?:\.\d*)?(?:[eE][+-]?\d+)?)?\b/ + }, + { + token : keywordMapper, + regex : "[a-zA-Z\\$_\u00a1-\uffff][a-zA-Z\\d\\$_\u00a1-\uffff]*" + }, + { token: "keyword", + regex: /λ/ + }, + { + token: "constant.language.boolean", + regex: "true|false" + }, + { + token : "punctuation.operator", + regex : /[.](?![.])/ + }, + { + token : "keyword.operator", + regex : /\|\||<=|>=|\.\.|\*\*|!=|:=|[=<>`!$%&*+\-~\/^]/, + next : "start" + }, + { + token : "punctuation.operator", + regex : /[?:,;.]/, + next : "start" + }, + { + token : "paren.lparen", + regex : /[\[({]/, + next : "start" + }, + { + token : "paren.rparen", + regex : /[\])}]/ + } + ], + "qqstring" : [ + { + token : "string", + regex : '"|$', + next : "start" + }, { + defaultToken: "string" + } + ], + "qstring" : [ + { + token : "string", + regex : "'|$", + next : "start" + }, { + defaultToken: "string" + } + ] + }; + }; + + oop.inherits(JSONataHighlightRules, TextHighlightRules); + + var TextMode = require("./text").Mode; + var Mode = function() { + this.HighlightRules = JSONataHighlightRules; + }; + oop.inherits(Mode, TextMode); + + + (function() { + this.createWorker = function(session) { + var worker = new WorkerClient(["ace"], "ace/mode/jsonata_worker", "JSONataWorker"); + worker.attachToDocument(session.getDocument()); + + worker.on("annotate", function(e) { + session.setAnnotations(e.data); + }); + + worker.on("terminate", function() { + session.clearAnnotations(); + }); + + return worker; + }; + + + this.$id = "ace/mode/jsonata"; + }).call(Mode.prototype); + + exports.Mode = Mode; + +}); diff --git a/editor/vendor/jsonata/worker-jsonata.js b/editor/vendor/jsonata/worker-jsonata.js new file mode 100644 index 000000000..b6803d3af --- /dev/null +++ b/editor/vendor/jsonata/worker-jsonata.js @@ -0,0 +1,4233 @@ +"no use strict"; +;(function(window) { +if (typeof window.window != "undefined" && window.document) + return; +if (window.require && window.define) + return; + +if (!window.console) { + window.console = function() { + var msgs = Array.prototype.slice.call(arguments, 0); + postMessage({type: "log", data: msgs}); + }; + window.console.error = + window.console.warn = + window.console.log = + window.console.trace = window.console; +} +window.window = window; +window.ace = window; + +window.onerror = function(message, file, line, col, err) { + postMessage({type: "error", data: { + message: message, + data: err.data, + file: file, + line: line, + col: col, + stack: err.stack + }}); +}; + +window.normalizeModule = function(parentId, moduleName) { + // normalize plugin requires + if (moduleName.indexOf("!") !== -1) { + var chunks = moduleName.split("!"); + return window.normalizeModule(parentId, chunks[0]) + "!" + window.normalizeModule(parentId, chunks[1]); + } + // normalize relative requires + if (moduleName.charAt(0) == ".") { + var base = parentId.split("/").slice(0, -1).join("/"); + moduleName = (base ? base + "/" : "") + moduleName; + + while (moduleName.indexOf(".") !== -1 && previous != moduleName) { + var previous = moduleName; + moduleName = moduleName.replace(/^\.\//, "").replace(/\/\.\//, "/").replace(/[^\/]+\/\.\.\//, ""); + } + } + + return moduleName; +}; + +window.require = function require(parentId, id) { + if (!id) { + id = parentId; + parentId = null; + } + if (!id.charAt) + throw new Error("worker.js require() accepts only (parentId, id) as arguments"); + + id = window.normalizeModule(parentId, id); + + var module = window.require.modules[id]; + if (module) { + if (!module.initialized) { + module.initialized = true; + module.exports = module.factory().exports; + } + return module.exports; + } + + if (!window.require.tlns) + return console.log("unable to load " + id); + + var path = resolveModuleId(id, window.require.tlns); + if (path.slice(-3) != ".js") path += ".js"; + + window.require.id = id; + window.require.modules[id] = {}; // prevent infinite loop on broken modules + importScripts(path); + return window.require(parentId, id); +}; +function resolveModuleId(id, paths) { + var testPath = id, tail = ""; + while (testPath) { + var alias = paths[testPath]; + if (typeof alias == "string") { + return alias + tail; + } else if (alias) { + return alias.location.replace(/\/*$/, "/") + (tail || alias.main || alias.name); + } else if (alias === false) { + return ""; + } + var i = testPath.lastIndexOf("/"); + if (i === -1) break; + tail = testPath.substr(i) + tail; + testPath = testPath.slice(0, i); + } + return id; +} +window.require.modules = {}; +window.require.tlns = {}; + +window.define = function(id, deps, factory) { + if (arguments.length == 2) { + factory = deps; + if (typeof id != "string") { + deps = id; + id = window.require.id; + } + } else if (arguments.length == 1) { + factory = id; + deps = []; + id = window.require.id; + } + + if (typeof factory != "function") { + window.require.modules[id] = { + exports: factory, + initialized: true + }; + return; + } + + if (!deps.length) + // If there is no dependencies, we inject "require", "exports" and + // "module" as dependencies, to provide CommonJS compatibility. + deps = ["require", "exports", "module"]; + + var req = function(childId) { + return window.require(id, childId); + }; + + window.require.modules[id] = { + exports: {}, + factory: function() { + var module = this; + var returnExports = factory.apply(this, deps.map(function(dep) { + switch (dep) { + // Because "require", "exports" and "module" aren't actual + // dependencies, we must handle them seperately. + case "require": return req; + case "exports": return module.exports; + case "module": return module; + // But for all other dependencies, we can just go ahead and + // require them. + default: return req(dep); + } + })); + if (returnExports) + module.exports = returnExports; + return module; + } + }; +}; +window.define.amd = {}; +require.tlns = {}; +window.initBaseUrls = function initBaseUrls(topLevelNamespaces) { + for (var i in topLevelNamespaces) + require.tlns[i] = topLevelNamespaces[i]; +}; + +window.initSender = function initSender() { + + var EventEmitter = window.require("ace/lib/event_emitter").EventEmitter; + var oop = window.require("ace/lib/oop"); + + var Sender = function() {}; + + (function() { + + oop.implement(this, EventEmitter); + + this.callback = function(data, callbackId) { + postMessage({ + type: "call", + id: callbackId, + data: data + }); + }; + + this.emit = function(name, data) { + postMessage({ + type: "event", + name: name, + data: data + }); + }; + + }).call(Sender.prototype); + + return new Sender(); +}; + +var main = window.main = null; +var sender = window.sender = null; + +window.onmessage = function(e) { + var msg = e.data; + if (msg.event && sender) { + sender._signal(msg.event, msg.data); + } + else if (msg.command) { + if (main[msg.command]) + main[msg.command].apply(main, msg.args); + else if (window[msg.command]) + window[msg.command].apply(window, msg.args); + else + throw new Error("Unknown command:" + msg.command); + } + else if (msg.init) { + window.initBaseUrls(msg.tlns); + require("ace/lib/es5-shim"); + sender = window.sender = window.initSender(); + var clazz = require(msg.module)[msg.classname]; + main = window.main = new clazz(sender); + } +}; +})(this); + +define("ace/lib/oop",["require","exports","module"], function(require, exports, module) { +"use strict"; + +exports.inherits = function(ctor, superCtor) { + ctor.super_ = superCtor; + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); +}; + +exports.mixin = function(obj, mixin) { + for (var key in mixin) { + obj[key] = mixin[key]; + } + return obj; +}; + +exports.implement = function(proto, mixin) { + exports.mixin(proto, mixin); +}; + +}); + +define("ace/range",["require","exports","module"], function(require, exports, module) { +"use strict"; +var comparePoints = function(p1, p2) { + return p1.row - p2.row || p1.column - p2.column; +}; +var Range = function(startRow, startColumn, endRow, endColumn) { + this.start = { + row: startRow, + column: startColumn + }; + + this.end = { + row: endRow, + column: endColumn + }; +}; + +(function() { + this.isEqual = function(range) { + return this.start.row === range.start.row && + this.end.row === range.end.row && + this.start.column === range.start.column && + this.end.column === range.end.column; + }; + this.toString = function() { + return ("Range: [" + this.start.row + "/" + this.start.column + + "] -> [" + this.end.row + "/" + this.end.column + "]"); + }; + + this.contains = function(row, column) { + return this.compare(row, column) == 0; + }; + this.compareRange = function(range) { + var cmp, + end = range.end, + start = range.start; + + cmp = this.compare(end.row, end.column); + if (cmp == 1) { + cmp = this.compare(start.row, start.column); + if (cmp == 1) { + return 2; + } else if (cmp == 0) { + return 1; + } else { + return 0; + } + } else if (cmp == -1) { + return -2; + } else { + cmp = this.compare(start.row, start.column); + if (cmp == -1) { + return -1; + } else if (cmp == 1) { + return 42; + } else { + return 0; + } + } + }; + this.comparePoint = function(p) { + return this.compare(p.row, p.column); + }; + this.containsRange = function(range) { + return this.comparePoint(range.start) == 0 && this.comparePoint(range.end) == 0; + }; + this.intersects = function(range) { + var cmp = this.compareRange(range); + return (cmp == -1 || cmp == 0 || cmp == 1); + }; + this.isEnd = function(row, column) { + return this.end.row == row && this.end.column == column; + }; + this.isStart = function(row, column) { + return this.start.row == row && this.start.column == column; + }; + this.setStart = function(row, column) { + if (typeof row == "object") { + this.start.column = row.column; + this.start.row = row.row; + } else { + this.start.row = row; + this.start.column = column; + } + }; + this.setEnd = function(row, column) { + if (typeof row == "object") { + this.end.column = row.column; + this.end.row = row.row; + } else { + this.end.row = row; + this.end.column = column; + } + }; + this.inside = function(row, column) { + if (this.compare(row, column) == 0) { + if (this.isEnd(row, column) || this.isStart(row, column)) { + return false; + } else { + return true; + } + } + return false; + }; + this.insideStart = function(row, column) { + if (this.compare(row, column) == 0) { + if (this.isEnd(row, column)) { + return false; + } else { + return true; + } + } + return false; + }; + this.insideEnd = function(row, column) { + if (this.compare(row, column) == 0) { + if (this.isStart(row, column)) { + return false; + } else { + return true; + } + } + return false; + }; + this.compare = function(row, column) { + if (!this.isMultiLine()) { + if (row === this.start.row) { + return column < this.start.column ? -1 : (column > this.end.column ? 1 : 0); + } + } + + if (row < this.start.row) + return -1; + + if (row > this.end.row) + return 1; + + if (this.start.row === row) + return column >= this.start.column ? 0 : -1; + + if (this.end.row === row) + return column <= this.end.column ? 0 : 1; + + return 0; + }; + this.compareStart = function(row, column) { + if (this.start.row == row && this.start.column == column) { + return -1; + } else { + return this.compare(row, column); + } + }; + this.compareEnd = function(row, column) { + if (this.end.row == row && this.end.column == column) { + return 1; + } else { + return this.compare(row, column); + } + }; + this.compareInside = function(row, column) { + if (this.end.row == row && this.end.column == column) { + return 1; + } else if (this.start.row == row && this.start.column == column) { + return -1; + } else { + return this.compare(row, column); + } + }; + this.clipRows = function(firstRow, lastRow) { + if (this.end.row > lastRow) + var end = {row: lastRow + 1, column: 0}; + else if (this.end.row < firstRow) + var end = {row: firstRow, column: 0}; + + if (this.start.row > lastRow) + var start = {row: lastRow + 1, column: 0}; + else if (this.start.row < firstRow) + var start = {row: firstRow, column: 0}; + + return Range.fromPoints(start || this.start, end || this.end); + }; + this.extend = function(row, column) { + var cmp = this.compare(row, column); + + if (cmp == 0) + return this; + else if (cmp == -1) + var start = {row: row, column: column}; + else + var end = {row: row, column: column}; + + return Range.fromPoints(start || this.start, end || this.end); + }; + + this.isEmpty = function() { + return (this.start.row === this.end.row && this.start.column === this.end.column); + }; + this.isMultiLine = function() { + return (this.start.row !== this.end.row); + }; + this.clone = function() { + return Range.fromPoints(this.start, this.end); + }; + this.collapseRows = function() { + if (this.end.column == 0) + return new Range(this.start.row, 0, Math.max(this.start.row, this.end.row-1), 0) + else + return new Range(this.start.row, 0, this.end.row, 0) + }; + this.toScreenRange = function(session) { + var screenPosStart = session.documentToScreenPosition(this.start); + var screenPosEnd = session.documentToScreenPosition(this.end); + + return new Range( + screenPosStart.row, screenPosStart.column, + screenPosEnd.row, screenPosEnd.column + ); + }; + this.moveBy = function(row, column) { + this.start.row += row; + this.start.column += column; + this.end.row += row; + this.end.column += column; + }; + +}).call(Range.prototype); +Range.fromPoints = function(start, end) { + return new Range(start.row, start.column, end.row, end.column); +}; +Range.comparePoints = comparePoints; + +Range.comparePoints = function(p1, p2) { + return p1.row - p2.row || p1.column - p2.column; +}; + + +exports.Range = Range; +}); + +define("ace/apply_delta",["require","exports","module"], function(require, exports, module) { +"use strict"; + +function throwDeltaError(delta, errorText){ + console.log("Invalid Delta:", delta); + throw "Invalid Delta: " + errorText; +} + +function positionInDocument(docLines, position) { + return position.row >= 0 && position.row < docLines.length && + position.column >= 0 && position.column <= docLines[position.row].length; +} + +function validateDelta(docLines, delta) { + if (delta.action != "insert" && delta.action != "remove") + throwDeltaError(delta, "delta.action must be 'insert' or 'remove'"); + if (!(delta.lines instanceof Array)) + throwDeltaError(delta, "delta.lines must be an Array"); + if (!delta.start || !delta.end) + throwDeltaError(delta, "delta.start/end must be an present"); + var start = delta.start; + if (!positionInDocument(docLines, delta.start)) + throwDeltaError(delta, "delta.start must be contained in document"); + var end = delta.end; + if (delta.action == "remove" && !positionInDocument(docLines, end)) + throwDeltaError(delta, "delta.end must contained in document for 'remove' actions"); + var numRangeRows = end.row - start.row; + var numRangeLastLineChars = (end.column - (numRangeRows == 0 ? start.column : 0)); + if (numRangeRows != delta.lines.length - 1 || delta.lines[numRangeRows].length != numRangeLastLineChars) + throwDeltaError(delta, "delta.range must match delta lines"); +} + +exports.applyDelta = function(docLines, delta, doNotValidate) { + + var row = delta.start.row; + var startColumn = delta.start.column; + var line = docLines[row] || ""; + switch (delta.action) { + case "insert": + var lines = delta.lines; + if (lines.length === 1) { + docLines[row] = line.substring(0, startColumn) + delta.lines[0] + line.substring(startColumn); + } else { + var args = [row, 1].concat(delta.lines); + docLines.splice.apply(docLines, args); + docLines[row] = line.substring(0, startColumn) + docLines[row]; + docLines[row + delta.lines.length - 1] += line.substring(startColumn); + } + break; + case "remove": + var endColumn = delta.end.column; + var endRow = delta.end.row; + if (row === endRow) { + docLines[row] = line.substring(0, startColumn) + line.substring(endColumn); + } else { + docLines.splice( + row, endRow - row + 1, + line.substring(0, startColumn) + docLines[endRow].substring(endColumn) + ); + } + break; + } +} +}); + +define("ace/lib/event_emitter",["require","exports","module"], function(require, exports, module) { +"use strict"; + +var EventEmitter = {}; +var stopPropagation = function() { this.propagationStopped = true; }; +var preventDefault = function() { this.defaultPrevented = true; }; + +EventEmitter._emit = +EventEmitter._dispatchEvent = function(eventName, e) { + this._eventRegistry || (this._eventRegistry = {}); + this._defaultHandlers || (this._defaultHandlers = {}); + + var listeners = this._eventRegistry[eventName] || []; + var defaultHandler = this._defaultHandlers[eventName]; + if (!listeners.length && !defaultHandler) + return; + + if (typeof e != "object" || !e) + e = {}; + + if (!e.type) + e.type = eventName; + if (!e.stopPropagation) + e.stopPropagation = stopPropagation; + if (!e.preventDefault) + e.preventDefault = preventDefault; + + listeners = listeners.slice(); + for (var i=0; i this.row) + return; + + var point = $getTransformedPoint(delta, {row: this.row, column: this.column}, this.$insertRight); + this.setPosition(point.row, point.column, true); + }; + + function $pointsInOrder(point1, point2, equalPointsInOrder) { + var bColIsAfter = equalPointsInOrder ? point1.column <= point2.column : point1.column < point2.column; + return (point1.row < point2.row) || (point1.row == point2.row && bColIsAfter); + } + + function $getTransformedPoint(delta, point, moveIfEqual) { + var deltaIsInsert = delta.action == "insert"; + var deltaRowShift = (deltaIsInsert ? 1 : -1) * (delta.end.row - delta.start.row); + var deltaColShift = (deltaIsInsert ? 1 : -1) * (delta.end.column - delta.start.column); + var deltaStart = delta.start; + var deltaEnd = deltaIsInsert ? deltaStart : delta.end; // Collapse insert range. + if ($pointsInOrder(point, deltaStart, moveIfEqual)) { + return { + row: point.row, + column: point.column + }; + } + if ($pointsInOrder(deltaEnd, point, !moveIfEqual)) { + return { + row: point.row + deltaRowShift, + column: point.column + (point.row == deltaEnd.row ? deltaColShift : 0) + }; + } + + return { + row: deltaStart.row, + column: deltaStart.column + }; + } + this.setPosition = function(row, column, noClip) { + var pos; + if (noClip) { + pos = { + row: row, + column: column + }; + } else { + pos = this.$clipPositionToDocument(row, column); + } + + if (this.row == pos.row && this.column == pos.column) + return; + + var old = { + row: this.row, + column: this.column + }; + + this.row = pos.row; + this.column = pos.column; + this._signal("change", { + old: old, + value: pos + }); + }; + this.detach = function() { + this.document.removeEventListener("change", this.$onChange); + }; + this.attach = function(doc) { + this.document = doc || this.document; + this.document.on("change", this.$onChange); + }; + this.$clipPositionToDocument = function(row, column) { + var pos = {}; + + if (row >= this.document.getLength()) { + pos.row = Math.max(0, this.document.getLength() - 1); + pos.column = this.document.getLine(pos.row).length; + } + else if (row < 0) { + pos.row = 0; + pos.column = 0; + } + else { + pos.row = row; + pos.column = Math.min(this.document.getLine(pos.row).length, Math.max(0, column)); + } + + if (column < 0) + pos.column = 0; + + return pos; + }; + +}).call(Anchor.prototype); + +}); + +define("ace/document",["require","exports","module","ace/lib/oop","ace/apply_delta","ace/lib/event_emitter","ace/range","ace/anchor"], function(require, exports, module) { +"use strict"; + +var oop = require("./lib/oop"); +var applyDelta = require("./apply_delta").applyDelta; +var EventEmitter = require("./lib/event_emitter").EventEmitter; +var Range = require("./range").Range; +var Anchor = require("./anchor").Anchor; + +var Document = function(textOrLines) { + this.$lines = [""]; + if (textOrLines.length === 0) { + this.$lines = [""]; + } else if (Array.isArray(textOrLines)) { + this.insertMergedLines({row: 0, column: 0}, textOrLines); + } else { + this.insert({row: 0, column:0}, textOrLines); + } +}; + +(function() { + + oop.implement(this, EventEmitter); + this.setValue = function(text) { + var len = this.getLength() - 1; + this.remove(new Range(0, 0, len, this.getLine(len).length)); + this.insert({row: 0, column: 0}, text); + }; + this.getValue = function() { + return this.getAllLines().join(this.getNewLineCharacter()); + }; + this.createAnchor = function(row, column) { + return new Anchor(this, row, column); + }; + if ("aaa".split(/a/).length === 0) { + this.$split = function(text) { + return text.replace(/\r\n|\r/g, "\n").split("\n"); + }; + } else { + this.$split = function(text) { + return text.split(/\r\n|\r|\n/); + }; + } + + + this.$detectNewLine = function(text) { + var match = text.match(/^.*?(\r\n|\r|\n)/m); + this.$autoNewLine = match ? match[1] : "\n"; + this._signal("changeNewLineMode"); + }; + this.getNewLineCharacter = function() { + switch (this.$newLineMode) { + case "windows": + return "\r\n"; + case "unix": + return "\n"; + default: + return this.$autoNewLine || "\n"; + } + }; + + this.$autoNewLine = ""; + this.$newLineMode = "auto"; + this.setNewLineMode = function(newLineMode) { + if (this.$newLineMode === newLineMode) + return; + + this.$newLineMode = newLineMode; + this._signal("changeNewLineMode"); + }; + this.getNewLineMode = function() { + return this.$newLineMode; + }; + this.isNewLine = function(text) { + return (text == "\r\n" || text == "\r" || text == "\n"); + }; + this.getLine = function(row) { + return this.$lines[row] || ""; + }; + this.getLines = function(firstRow, lastRow) { + return this.$lines.slice(firstRow, lastRow + 1); + }; + this.getAllLines = function() { + return this.getLines(0, this.getLength()); + }; + this.getLength = function() { + return this.$lines.length; + }; + this.getTextRange = function(range) { + return this.getLinesForRange(range).join(this.getNewLineCharacter()); + }; + this.getLinesForRange = function(range) { + var lines; + if (range.start.row === range.end.row) { + lines = [this.getLine(range.start.row).substring(range.start.column, range.end.column)]; + } else { + lines = this.getLines(range.start.row, range.end.row); + lines[0] = (lines[0] || "").substring(range.start.column); + var l = lines.length - 1; + if (range.end.row - range.start.row == l) + lines[l] = lines[l].substring(0, range.end.column); + } + return lines; + }; + this.insertLines = function(row, lines) { + console.warn("Use of document.insertLines is deprecated. Use the insertFullLines method instead."); + return this.insertFullLines(row, lines); + }; + this.removeLines = function(firstRow, lastRow) { + console.warn("Use of document.removeLines is deprecated. Use the removeFullLines method instead."); + return this.removeFullLines(firstRow, lastRow); + }; + this.insertNewLine = function(position) { + console.warn("Use of document.insertNewLine is deprecated. Use insertMergedLines(position, ['', '']) instead."); + return this.insertMergedLines(position, ["", ""]); + }; + this.insert = function(position, text) { + if (this.getLength() <= 1) + this.$detectNewLine(text); + + return this.insertMergedLines(position, this.$split(text)); + }; + this.insertInLine = function(position, text) { + var start = this.clippedPos(position.row, position.column); + var end = this.pos(position.row, position.column + text.length); + + this.applyDelta({ + start: start, + end: end, + action: "insert", + lines: [text] + }, true); + + return this.clonePos(end); + }; + + this.clippedPos = function(row, column) { + var length = this.getLength(); + if (row === undefined) { + row = length; + } else if (row < 0) { + row = 0; + } else if (row >= length) { + row = length - 1; + column = undefined; + } + var line = this.getLine(row); + if (column == undefined) + column = line.length; + column = Math.min(Math.max(column, 0), line.length); + return {row: row, column: column}; + }; + + this.clonePos = function(pos) { + return {row: pos.row, column: pos.column}; + }; + + this.pos = function(row, column) { + return {row: row, column: column}; + }; + + this.$clipPosition = function(position) { + var length = this.getLength(); + if (position.row >= length) { + position.row = Math.max(0, length - 1); + position.column = this.getLine(length - 1).length; + } else { + position.row = Math.max(0, position.row); + position.column = Math.min(Math.max(position.column, 0), this.getLine(position.row).length); + } + return position; + }; + this.insertFullLines = function(row, lines) { + row = Math.min(Math.max(row, 0), this.getLength()); + var column = 0; + if (row < this.getLength()) { + lines = lines.concat([""]); + column = 0; + } else { + lines = [""].concat(lines); + row--; + column = this.$lines[row].length; + } + this.insertMergedLines({row: row, column: column}, lines); + }; + this.insertMergedLines = function(position, lines) { + var start = this.clippedPos(position.row, position.column); + var end = { + row: start.row + lines.length - 1, + column: (lines.length == 1 ? start.column : 0) + lines[lines.length - 1].length + }; + + this.applyDelta({ + start: start, + end: end, + action: "insert", + lines: lines + }); + + return this.clonePos(end); + }; + this.remove = function(range) { + var start = this.clippedPos(range.start.row, range.start.column); + var end = this.clippedPos(range.end.row, range.end.column); + this.applyDelta({ + start: start, + end: end, + action: "remove", + lines: this.getLinesForRange({start: start, end: end}) + }); + return this.clonePos(start); + }; + this.removeInLine = function(row, startColumn, endColumn) { + var start = this.clippedPos(row, startColumn); + var end = this.clippedPos(row, endColumn); + + this.applyDelta({ + start: start, + end: end, + action: "remove", + lines: this.getLinesForRange({start: start, end: end}) + }, true); + + return this.clonePos(start); + }; + this.removeFullLines = function(firstRow, lastRow) { + firstRow = Math.min(Math.max(0, firstRow), this.getLength() - 1); + lastRow = Math.min(Math.max(0, lastRow ), this.getLength() - 1); + var deleteFirstNewLine = lastRow == this.getLength() - 1 && firstRow > 0; + var deleteLastNewLine = lastRow < this.getLength() - 1; + var startRow = ( deleteFirstNewLine ? firstRow - 1 : firstRow ); + var startCol = ( deleteFirstNewLine ? this.getLine(startRow).length : 0 ); + var endRow = ( deleteLastNewLine ? lastRow + 1 : lastRow ); + var endCol = ( deleteLastNewLine ? 0 : this.getLine(endRow).length ); + var range = new Range(startRow, startCol, endRow, endCol); + var deletedLines = this.$lines.slice(firstRow, lastRow + 1); + + this.applyDelta({ + start: range.start, + end: range.end, + action: "remove", + lines: this.getLinesForRange(range) + }); + return deletedLines; + }; + this.removeNewLine = function(row) { + if (row < this.getLength() - 1 && row >= 0) { + this.applyDelta({ + start: this.pos(row, this.getLine(row).length), + end: this.pos(row + 1, 0), + action: "remove", + lines: ["", ""] + }); + } + }; + this.replace = function(range, text) { + if (!(range instanceof Range)) + range = Range.fromPoints(range.start, range.end); + if (text.length === 0 && range.isEmpty()) + return range.start; + if (text == this.getTextRange(range)) + return range.end; + + this.remove(range); + var end; + if (text) { + end = this.insert(range.start, text); + } + else { + end = range.start; + } + + return end; + }; + this.applyDeltas = function(deltas) { + for (var i=0; i=0; i--) { + this.revertDelta(deltas[i]); + } + }; + this.applyDelta = function(delta, doNotValidate) { + var isInsert = delta.action == "insert"; + if (isInsert ? delta.lines.length <= 1 && !delta.lines[0] + : !Range.comparePoints(delta.start, delta.end)) { + return; + } + + if (isInsert && delta.lines.length > 20000) + this.$splitAndapplyLargeDelta(delta, 20000); + applyDelta(this.$lines, delta, doNotValidate); + this._signal("change", delta); + }; + + this.$splitAndapplyLargeDelta = function(delta, MAX) { + var lines = delta.lines; + var l = lines.length; + var row = delta.start.row; + var column = delta.start.column; + var from = 0, to = 0; + do { + from = to; + to += MAX - 1; + var chunk = lines.slice(from, to); + if (to > l) { + delta.lines = chunk; + delta.start.row = row + from; + delta.start.column = column; + break; + } + chunk.push(""); + this.applyDelta({ + start: this.pos(row + from, column), + end: this.pos(row + to, column = 0), + action: delta.action, + lines: chunk + }, true); + } while(true); + }; + this.revertDelta = function(delta) { + this.applyDelta({ + start: this.clonePos(delta.start), + end: this.clonePos(delta.end), + action: (delta.action == "insert" ? "remove" : "insert"), + lines: delta.lines.slice() + }); + }; + this.indexToPosition = function(index, startRow) { + var lines = this.$lines || this.getAllLines(); + var newlineLength = this.getNewLineCharacter().length; + for (var i = startRow || 0, l = lines.length; i < l; i++) { + index -= lines[i].length + newlineLength; + if (index < 0) + return {row: i, column: index + lines[i].length + newlineLength}; + } + return {row: l-1, column: lines[l-1].length}; + }; + this.positionToIndex = function(pos, startRow) { + var lines = this.$lines || this.getAllLines(); + var newlineLength = this.getNewLineCharacter().length; + var index = 0; + var row = Math.min(pos.row, lines.length); + for (var i = startRow || 0; i < row; ++i) + index += lines[i].length + newlineLength; + + return index + pos.column; + }; + +}).call(Document.prototype); + +exports.Document = Document; +}); + +define("ace/lib/lang",["require","exports","module"], function(require, exports, module) { +"use strict"; + +exports.last = function(a) { + return a[a.length - 1]; +}; + +exports.stringReverse = function(string) { + return string.split("").reverse().join(""); +}; + +exports.stringRepeat = function (string, count) { + var result = ''; + while (count > 0) { + if (count & 1) + result += string; + + if (count >>= 1) + string += string; + } + return result; +}; + +var trimBeginRegexp = /^\s\s*/; +var trimEndRegexp = /\s\s*$/; + +exports.stringTrimLeft = function (string) { + return string.replace(trimBeginRegexp, ''); +}; + +exports.stringTrimRight = function (string) { + return string.replace(trimEndRegexp, ''); +}; + +exports.copyObject = function(obj) { + var copy = {}; + for (var key in obj) { + copy[key] = obj[key]; + } + return copy; +}; + +exports.copyArray = function(array){ + var copy = []; + for (var i=0, l=array.length; i': 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' + }; + 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); + while (position < length && ' \t\n\r\v'.indexOf(currentChar) > -1) { + position++; + currentChar = path.charAt(position); + } + 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) === '=') { + 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 (operators.hasOwnProperty(currentChar)) { + position++; + return create('operator', currentChar); + } + if (currentChar === '"' || currentChar === "'") { + var quoteType = currentChar; + 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') { + 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 at column " + position, + stack: (new Error()).stack, + position: position + }; + } + } else { + throw { + message: 'unsupported escape sequence: \\' + currentChar + ' at column ' + position, + 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 at column ' + position, + stack: (new Error()).stack, + position: position + }; + } + 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] + ' at column ' + position, + stack: (new Error()).stack, + position: position, + token: match[0] + }; + } + } + 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) === '$') { + 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 === '') { + return null; + } + return create('name', name); + } + } + } else { + i++; + } + } + }; + + return next; + }; + + 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)') { + msg = "Syntax error: expected '" + id + "' before end of expression"; + } else { + msg = "Syntax error: expected '" + id + "', got '" + node.id + "' at column " + node.position; + } + 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 + " at column " + next_token.position, + stack: (new Error()).stack, + position: next_token.position, + token: value + }; + } + break; + case 'string': + case 'number': + case 'value': + type = "literal"; + symbol = symbol_table["(literal)"]; + break; + default: + throw { + message: "Unexpected token:" + value + " at column " + next_token.position, + 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; + }; + 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; + }; + 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; + }; + 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; + }; + 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 + prefix('*', function () { + this.type = "wildcard"; + return this; + }); + prefix('**', function () { + this.type = "descendant"; + return this; + }); + infix("(", operators['('], function (left) { + this.procedure = left; + this.type = 'function'; + this.arguments = []; + if (node.id !== ')') { + for (;;) { + if (node.type === 'operator' && node.id === '?') { + this.type = 'partial'; + this.arguments.push(node); + advance('?'); + } else { + this.arguments.push(expression(0)); + } + if (node.id !== ',') break; + advance(','); + } + } + advance(")"); + if (left.type === 'name' && (left.value === 'function' || left.value === '\u03BB')) { + 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'; + advance('{'); + this.body = expression(0); + advance('}'); + } + return this; + }); + 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; + }); + prefix("{", function () { + 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("}"); + this.lhs = a; + this.type = "unary"; + return this; + }); + prefix("[", function () { + var a = []; + if (node.id !== "]") { + for (;;) { + var item = expression(0); + if (node.id === "..") { + 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; + }); + infix("[", operators['['], function (left) { + this.lhs = left; + this.rhs = expression(operators[']']); + this.type = 'binary'; + advance("]"); + return this; + }); + infix("{", operators['{'], function (left) { + this.lhs = left; + this.rhs = expression(operators['}']); + this.type = 'binary'; + advance("}"); + return this; + }); + infix("?", operators['?'], function (left) { + this.type = 'condition'; + this.condition = left; + this.then = expression(0); + if (node.id === ':') { + advance(":"); + this.else = expression(0); + } + return this; + }); + 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') { + expr.then = tail_call_optimize(expr.then); + expr.else = tail_call_optimize(expr.else); + result = expr; + } else if(expr.type === '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; + }; + var post_parse = function (expr) { + var result = []; + switch (expr.type) { + case 'binary': + switch (expr.value) { + case '.': + var step = post_parse(expr.lhs); + if (Array.isArray(step)) { + Array.prototype.push.apply(result, step); + } else { + result.push(step); + } + var rest = [post_parse(expr.rhs)]; + Array.prototype.push.apply(result, rest); + result.type = 'path'; + break; + case '[': + result = post_parse(expr.lhs); + if (typeof result.aggregate !== 'undefined') { + throw { + message: 'A predicate cannot follow an aggregate in a step. Error at column: ' + expr.position, + stack: (new Error()).stack, + position: expr.position + }; + } + if (typeof result.predicate === 'undefined') { + result.predicate = []; + } + result.predicate.push(post_parse(expr.rhs)); + break; + case '{': + result = post_parse(expr.lhs); + if (typeof result.aggregate !== 'undefined') { + throw { + message: 'Each step can only have one aggregator. Error at column: ' + expr.position, + stack: (new Error()).stack, + position: expr.position + }; + } + result.aggregate = post_parse(expr.rhs); + 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 === '[') { + result.lhs = expr.lhs.map(function (item) { + return post_parse(item); + }); + } else if (expr.value === '{') { + result.lhs = expr.lhs.map(function (pair) { + return [post_parse(pair[0]), post_parse(pair[1])]; + }); + } else { + result.expression = post_parse(expr.expression); + 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}; + result.expressions = expr.expressions.map(function (item) { + return post_parse(item); + }); + break; + case 'name': + case 'literal': + case 'wildcard': + case 'descendant': + case 'variable': + result = expr; + break; + case 'operator': + if (expr.value === 'and' || expr.value === 'or' || expr.value === 'in') { + expr.type = 'name'; + result = post_parse(expr); + } else if (expr.value === '?') { + result = expr; + } else { + throw { + message: "Syntax error: " + expr.value + " at column " + expr.position, + stack: (new Error()).stack, + position: expr.position, + token: expr.value + }; + } + break; + default: + var reason = "Unknown expression type: " + expr.value + " at column " + expr.position; + 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; + }; + + lexer = tokenizer(source); + advance(); + var expr = expression(0); + if (node.id !== '(end)') { + throw { + message: "Syntax error: " + node.value + " at column " + node.position, + stack: (new Error()).stack, + position: node.position, + token: node.value + }; + } + expr = post_parse(expr); + if (expr.type === 'name') { + expr = [expr]; + expr.type = 'path'; + } + + return expr; + }; + 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; + } + function isArrayOfNumbers(arg) { + var result = false; + if(Array.isArray(arg)) { + result = (arg.filter(function(item){return !isNumeric(item);}).length == 0); + } + return result; + } + Number.isInteger = Number.isInteger || function(value) { + return typeof value === "number" && + isFinite(value) && + Math.floor(value) === value; + }; + 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('aggregate')) { + result = applyAggregate(expr.aggregate, result, environment); + } + + var exitCallback = environment.lookup('__evaluate_exit'); + if(exitCallback) { + exitCallback(expr, input, environment, result); + } + + return result; + } + function evaluatePath(expr, input, environment) { + var result; + var inputSequence; + if (expr[0].type === 'variable') { + expr[0].absolute = true; + } else if(expr[0].type === 'unary' && expr[0].value === '[') { + input = [null];// dummy singleton sequence for first step + } + expr.forEach(function (step) { + var resultSequence = []; + result = undefined; + if (step.absolute === true) { + inputSequence = [input]; // dummy singleton sequence for first (absolute) step + } else if (Array.isArray(input)) { + inputSequence = input; + } else { + inputSequence = [input]; + } + if (expr.length > 1 && step.type === 'literal') { + step.type = 'name'; + } + if (step.value === '{') { + if(typeof input !== 'undefined') { + result = evaluateGroupExpression(step, inputSequence, environment); + } + } else { + inputSequence.forEach(function (item) { + var res = evaluate(step, item, environment); + if (typeof res !== 'undefined') { + if (Array.isArray(res)) { + res.forEach(function (innerRes) { + if (typeof innerRes !== 'undefined') { + resultSequence.push(innerRes); + } + }); + } else { + resultSequence.push(res); + } + } + }); + if (resultSequence.length == 1) { + result = resultSequence[0]; + } else if (resultSequence.length > 1) { + result = resultSequence; + } + } + + input = result; + }); + return result; + } + function applyPredicates(predicates, input, environment) { + var result; + var inputSequence = input; + + var results = []; + predicates.forEach(function (predicate) { + if (!Array.isArray(inputSequence)) { + inputSequence = [inputSequence]; + } + results = []; + result = undefined; + if (predicate.type === 'literal' && isNumeric(predicate.value)) { + var index = predicate.value; + if (!Number.isInteger(index)) { + index = Math.floor(index); + } + if (index < 0) { + 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)) { + ires = Math.floor(ires); + } + if (ires < 0) { + 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; + } + function applyAggregate(expr, input, environment) { + var result = {}; + if (Array.isArray(input)) { + var aggEnv = createFrame(environment); + aggEnv.bind('_', input[0]); + for (var index = 1; index < input.length; index++) { + var reduce = evaluate(expr, input[index], aggEnv); + aggEnv.bind('_', reduce); + } + result = aggEnv.lookup('_'); + } else { + result = input; + } + return result; + } + 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; + } + 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 + " at column " + expr.position, + stack: (new Error()).stack, + position: expr.position, + token: expr.value, + value: result + }; + } + break; + case '[': + result = []; + expr.lhs.forEach(function (item) { + var value = evaluate(item, input, environment); + if (typeof value !== 'undefined') { + if (item.value === '..') { + result = functionAppend(result, value); + } else { + result.push(value); + } + } + }); + break; + case '{': + result = evaluateGroupExpression(expr, input, environment); + break; + + } + return result; + } + function evaluateName(expr, input, environment) { + 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; + } + function evaluateLiteral(expr) { + return expr.value; + } + 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; + } + 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; + } + function evaluateDescendants(expr, input) { + var result; + var resultSequence = []; + if (typeof input !== 'undefined') { + recurseDescendants(input, resultSequence); + if (resultSequence.length == 1) { + result = resultSequence[0]; + } else { + result = resultSequence; + } + } + return result; + } + function recurseDescendants(input, results) { + 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); + }); + } + } + 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') { + return result; + } + + if (!isNumeric(lhs)) { + throw { + message: 'LHS of ' + expr.value + ' operator must evaluate to a number at column ' + expr.position, + 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 at column ' + expr.position, + 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; + } + 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') { + 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; + } + 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') { + 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; + } + 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; + } + 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; + } + function evaluateGroupExpression(expr, input, environment) { + var result = {}; + var groups = {}; + if (!Array.isArray(input)) { + input = [input]; + } + input.forEach(function (item) { + expr.lhs.forEach(function (pair) { + var key = evaluate(pair[0], item, environment); + if (typeof key !== 'string') { + throw { + message: 'Key in object structure must evaluate to a string. Got: ' + key + ' at column ' + expr.position, + stack: (new Error()).stack, + position: expr.position, + value: key + }; + } + var entry = {data: item, expr: pair[1]}; + if (groups.hasOwnProperty(key)) { + groups[key].data = functionAppend(groups[key].data, item); + } else { + groups[key] = entry; + } + }); + }); + for (var key in groups) { + var entry = groups[key]; + var value = evaluate(entry.expr, entry.data, environment); + result[key] = value; + } + return result; + } + 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') { + return result; + } + + if (lhs > rhs) { + return result; + } + + if (!Number.isInteger(lhs)) { + throw { + message: 'LHS of range operator (..) must evaluate to an integer at column ' + expr.position, + 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 at column ' + expr.position, + 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; + } + function evaluateBindExpression(expr, input, environment) { + 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 $) at column " + expr.position, + stack: (new Error()).stack, + position: expr.position, + token: expr.value, + value: expr.lhs.value + }; + } + environment.bind(expr.lhs.value, value); + return value; + } + 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; + } + function evaluateBlock(expr, input, environment) { + var result; + var frame = createFrame(environment); + expr.expressions.forEach(function (expression) { + result = evaluate(expression, input, frame); + }); + + return result; + } + function evaluateVariable(expr, input, environment) { + var result; + if (expr.value === '') { + result = input; + } else { + result = environment.lookup(expr.value); + } + return result; + } + function evaluateFunction(expr, input, environment) { + var result; + var evaluatedArgs = []; + expr.arguments.forEach(function (arg) { + evaluatedArgs.push(evaluate(arg, input, environment)); + }); + var proc = evaluate(expr.procedure, input, environment); + + if (typeof proc === 'undefined' && expr.procedure.type === 'name' && environment.lookup(expr.procedure.value)) { + throw { + message: 'Attempted to invoke a non-function at column ' + expr.position + '. Did you mean \'$' + expr.procedure.value + '\'?', + stack: (new Error()).stack, + position: expr.position, + token: expr.procedure.value + }; + } + try { + result = apply(proc, evaluatedArgs, environment, input); + while(typeof result === 'object' && result.lambda == true && result.thunk == true) { + 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) { + err.position = expr.position; + err.token = expr.procedure.value; + throw err; + } + return result; + } + 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; + } + function evaluateLambda(expr, input, environment) { + var procedure = { + lambda: true, + input: input, + environment: environment, + arguments: expr.arguments, + body: expr.body + }; + if(expr.thunk == true) { + procedure.thunk = true; + } + return procedure; + } + function evaluatePartialApplication(expr, input, environment) { + var result; + var evaluatedArgs = []; + expr.arguments.forEach(function (arg) { + if (arg.type === 'operator' && arg.value === '?') { + evaluatedArgs.push(arg); + } else { + evaluatedArgs.push(evaluate(arg, input, environment)); + } + }); + var proc = evaluate(expr.procedure, input, environment); + if (typeof proc === 'undefined' && expr.procedure.type === 'name' && environment.lookup(expr.procedure.value)) { + throw { + message: 'Attempted to partially apply a non-function at column ' + expr.position + '. Did you mean \'$' + expr.procedure.value + '\'?', + stack: (new Error()).stack, + position: expr.position, + token: expr.procedure.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 at column ' + expr.position, + stack: (new Error()).stack, + position: expr.position, + token: expr.procedure.value + }; + } + return result; + } + 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') { + result = applyNativeFunction(proc.body, env); + } else { + result = evaluate(proc.body, proc.input, env); + } + return result; + } + function partialApplyProcedure(proc, args) { + 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; + } + function partialApplyNativeFunction(native, args) { + 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; + } + function applyNativeFunction(proc, env) { + var sigArgs = getNativeFunctionArguments(proc); + var args = sigArgs.map(function (sigArg) { + return env.lookup(sigArg.trim()); + }); + + var result = proc.apply(null, args); + return result; + } + function getNativeFunctionArguments(func) { + var signature = func.toString(); + var sigParens = /\(([^\)]*)\)/.exec(signature)[1]; // the contents of the parens + var sigArgs = sigParens.split(','); + return sigArgs; + } + 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; + } + function functionSum(args) { + var total = 0; + + if (arguments.length != 1) { + throw { + message: 'The sum function expects one argument', + stack: (new Error()).stack + }; + } + if(typeof args === 'undefined') { + return undefined; + } + + if(!Array.isArray(args)) { + args = [args]; + } + 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; + } + function functionCount(args) { + if (arguments.length != 1) { + throw { + message: 'The count function expects one argument', + stack: (new Error()).stack + }; + } + if(typeof args === 'undefined') { + return 0; + } + + if(!Array.isArray(args)) { + args = [args]; + } + + return args.length; + } + function functionMax(args) { + var max; + + if (arguments.length != 1) { + throw { + message: 'The max function expects one argument', + stack: (new Error()).stack + }; + } + if(typeof args === 'undefined') { + return undefined; + } + + if(!Array.isArray(args)) { + args = [args]; + } + 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; + } + function functionMin(args) { + var min; + + if (arguments.length != 1) { + throw { + message: 'The min function expects one argument', + stack: (new Error()).stack + }; + } + if(typeof args === 'undefined') { + return undefined; + } + + if(!Array.isArray(args)) { + args = [args]; + } + 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; + } + function functionAverage(args) { + var total = 0; + + if (arguments.length != 1) { + throw { + message: 'The average function expects one argument', + stack: (new Error()).stack + }; + } + if(typeof args === 'undefined') { + return undefined; + } + + if(!Array.isArray(args)) { + args = [args]; + } + 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; + } + function functionString(arg) { + var str; + + if(arguments.length != 1) { + throw { + message: 'The string function expects one argument', + stack: (new Error()).stack + }; + } + if(typeof arg === 'undefined') { + return undefined; + } + + if (typeof arg === 'string') { + str = arg; + } else if(typeof arg === 'function' || isLambda(arg)) { + 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; + } + 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 + }; + } + if(typeof str === 'undefined') { + return undefined; + } + 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); + } + function functionSubstringBefore(str, chars) { + if(arguments.length != 2) { + throw { + message: 'The substringBefore function expects two arguments', + stack: (new Error()).stack + }; + } + if(typeof str === 'undefined') { + return undefined; + } + 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; + } + } + function functionSubstringAfter(str, chars) { + if(arguments.length != 2) { + throw { + message: 'The substringAfter function expects two arguments', + stack: (new Error()).stack + }; + } + if(typeof str === 'undefined') { + return undefined; + } + 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; + } + } + function functionLowercase(str) { + if(arguments.length != 1) { + throw { + message: 'The lowercase function expects one argument', + stack: (new Error()).stack + }; + } + if(typeof str === 'undefined') { + return undefined; + } + 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(); + } + function functionUppercase(str) { + if(arguments.length != 1) { + throw { + message: 'The uppercase function expects one argument', + stack: (new Error()).stack + }; + } + if(typeof str === 'undefined') { + return undefined; + } + 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(); + } + function functionLength(str) { + if(arguments.length != 1) { + throw { + message: 'The length function expects one argument', + stack: (new Error()).stack + }; + } + if(typeof str === 'undefined') { + return undefined; + } + 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; + } + 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 + }; + } + if(typeof str === 'undefined') { + return undefined; + } + if(typeof str !== 'string') { + throw { + message: 'Type error: first argument of split function must evaluate to a string', + stack: (new Error()).stack, + value: str + }; + } + if(typeof separator !== 'string') { + throw { + message: 'Type error: second argument of split function must evaluate to a string', + stack: (new Error()).stack, + value: separator + }; + } + 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); + } + 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 + }; + } + if(typeof strs === 'undefined') { + return undefined; + } + + if(!Array.isArray(strs)) { + strs = [strs]; + } + 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(typeof separator === 'undefined') { + separator = ""; + } + 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); + } + function functionNumber(arg) { + var result; + + if(arguments.length != 1) { + throw { + message: 'The number function expects one argument', + stack: (new Error()).stack + }; + } + if(typeof arg === 'undefined') { + return undefined; + } + + if (typeof arg === '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; + } + function functionBoolean(arg) { + + if(arguments.length != 1) { + throw { + message: 'The boolean function expects one argument', + stack: (new Error()).stack + }; + } + 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) { + if (!(isLambda(arg))) { + result = true; + } + } + } else if (typeof arg === 'boolean' && arg == true) { + result = true; + } + return result; + } + function functionNot(arg) { + return !functionBoolean(arg); + } + function functionMap(func) { + var varargs = arguments; + var result = []; + var args = []; + for (var ii = 1; ii < varargs.length; ii++) { + if (Array.isArray(varargs[ii])) { + args.push(varargs[ii]); + } else { + args.push([varargs[ii]]); + } + + } + 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]); + } + result.push(apply(func, func_args, null, null)); + } + } + return 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; + } + function functionKeys(arg) { + var result = []; + + if(Array.isArray(arg)) { + 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; + } + function functionLookup(object, key) { + var result = evaluateName({value: key}, object); + return result; + } + function functionAppend(arg1, arg2) { + if (typeof arg1 === 'undefined') { + return arg2; + } + if (typeof arg2 === 'undefined') { + return arg1; + } + if (!Array.isArray(arg1)) { + arg1 = [arg1]; + } + if (!Array.isArray(arg2)) { + arg2 = [arg2]; + } + Array.prototype.push.apply(arg1, arg2); + return arg1; + } + 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; + } + } + function functionSpread(arg) { + var result = []; + + if(Array.isArray(arg)) { + 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; + } + 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); + function jsonata(expr) { + var ast = parser(expr); + var environment = createFrame(staticFrame); + return { + evaluate: function (input, bindings) { + if (typeof bindings !== 'undefined') { + var exec_env; + exec_env = createFrame(environment); + for (var v in bindings) { + exec_env.bind(v, bindings[v]); + } + } else { + exec_env = environment; + } + exec_env.bind('$', input); + return evaluate(ast, input, exec_env); + }, + assign: function (name, value) { + environment.bind(name, value); + } + }; + } + + jsonata.parser = parser; + + return jsonata; + + })(); + + + + + var oop = require("../lib/oop"); + var Mirror = require("../worker/mirror").Mirror; + + var JSONataWorker = exports.JSONataWorker = function(sender) { + Mirror.call(this, sender); + this.setTimeout(200); + }; + + oop.inherits(JSONataWorker, Mirror); + + (function() { + + this.onUpdate = function() { + var value = this.doc.getValue(); + var errors = []; + try { + if (value) + jsonata(value); + } catch (e) { + var pos = this.doc.indexToPosition(e.position-1); + errors.push({ + row: pos.row, + column: pos.column, + text: e.message, + type: "error" + }); + } + this.sender.emit("annotate", errors); + }; + + }).call(JSONataWorker.prototype); + +}); + +define("ace/lib/es5-shim",["require","exports","module"], function(require, exports, module) { + +function Empty() {} + +if (!Function.prototype.bind) { + Function.prototype.bind = function bind(that) { // .length is 1 + var target = this; + if (typeof target != "function") { + throw new TypeError("Function.prototype.bind called on incompatible " + target); + } + var args = slice.call(arguments, 1); // for normal call + var bound = function () { + + if (this instanceof bound) { + + var result = target.apply( + this, + args.concat(slice.call(arguments)) + ); + if (Object(result) === result) { + return result; + } + return this; + + } else { + return target.apply( + that, + args.concat(slice.call(arguments)) + ); + + } + + }; + if(target.prototype) { + Empty.prototype = target.prototype; + bound.prototype = new Empty(); + Empty.prototype = null; + } + return bound; + }; +} +var call = Function.prototype.call; +var prototypeOfArray = Array.prototype; +var prototypeOfObject = Object.prototype; +var slice = prototypeOfArray.slice; +var _toString = call.bind(prototypeOfObject.toString); +var owns = call.bind(prototypeOfObject.hasOwnProperty); +var defineGetter; +var defineSetter; +var lookupGetter; +var lookupSetter; +var supportsAccessors; +if ((supportsAccessors = owns(prototypeOfObject, "__defineGetter__"))) { + defineGetter = call.bind(prototypeOfObject.__defineGetter__); + defineSetter = call.bind(prototypeOfObject.__defineSetter__); + lookupGetter = call.bind(prototypeOfObject.__lookupGetter__); + lookupSetter = call.bind(prototypeOfObject.__lookupSetter__); +} +if ([1,2].splice(0).length != 2) { + if(function() { // test IE < 9 to splice bug - see issue #138 + function makeArray(l) { + var a = new Array(l+2); + a[0] = a[1] = 0; + return a; + } + var array = [], lengthBefore; + + array.splice.apply(array, makeArray(20)); + array.splice.apply(array, makeArray(26)); + + lengthBefore = array.length; //46 + array.splice(5, 0, "XXX"); // add one element + + lengthBefore + 1 == array.length + + if (lengthBefore + 1 == array.length) { + return true;// has right splice implementation without bugs + } + }()) {//IE 6/7 + var array_splice = Array.prototype.splice; + Array.prototype.splice = function(start, deleteCount) { + if (!arguments.length) { + return []; + } else { + return array_splice.apply(this, [ + start === void 0 ? 0 : start, + deleteCount === void 0 ? (this.length - start) : deleteCount + ].concat(slice.call(arguments, 2))) + } + }; + } else {//IE8 + Array.prototype.splice = function(pos, removeCount){ + var length = this.length; + if (pos > 0) { + if (pos > length) + pos = length; + } else if (pos == void 0) { + pos = 0; + } else if (pos < 0) { + pos = Math.max(length + pos, 0); + } + + if (!(pos+removeCount < length)) + removeCount = length - pos; + + var removed = this.slice(pos, pos+removeCount); + var insert = slice.call(arguments, 2); + var add = insert.length; + if (pos === length) { + if (add) { + this.push.apply(this, insert); + } + } else { + var remove = Math.min(removeCount, length - pos); + var tailOldPos = pos + remove; + var tailNewPos = tailOldPos + add - remove; + var tailCount = length - tailOldPos; + var lengthAfterRemove = length - remove; + + if (tailNewPos < tailOldPos) { // case A + for (var i = 0; i < tailCount; ++i) { + this[tailNewPos+i] = this[tailOldPos+i]; + } + } else if (tailNewPos > tailOldPos) { // case B + for (i = tailCount; i--; ) { + this[tailNewPos+i] = this[tailOldPos+i]; + } + } // else, add == remove (nothing to do) + + if (add && pos === lengthAfterRemove) { + this.length = lengthAfterRemove; // truncate array + this.push.apply(this, insert); + } else { + this.length = lengthAfterRemove + add; // reserves space + for (i = 0; i < add; ++i) { + this[pos+i] = insert[i]; + } + } + } + return removed; + }; + } +} +if (!Array.isArray) { + Array.isArray = function isArray(obj) { + return _toString(obj) == "[object Array]"; + }; +} +var boxedString = Object("a"), + splitString = boxedString[0] != "a" || !(0 in boxedString); + +if (!Array.prototype.forEach) { + Array.prototype.forEach = function forEach(fun /*, thisp*/) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + thisp = arguments[1], + i = -1, + length = self.length >>> 0; + if (_toString(fun) != "[object Function]") { + throw new TypeError(); // TODO message + } + + while (++i < length) { + if (i in self) { + fun.call(thisp, self[i], i, object); + } + } + }; +} +if (!Array.prototype.map) { + Array.prototype.map = function map(fun /*, thisp*/) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0, + result = Array(length), + thisp = arguments[1]; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + + for (var i = 0; i < length; i++) { + if (i in self) + result[i] = fun.call(thisp, self[i], i, object); + } + return result; + }; +} +if (!Array.prototype.filter) { + Array.prototype.filter = function filter(fun /*, thisp */) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0, + result = [], + value, + thisp = arguments[1]; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + + for (var i = 0; i < length; i++) { + if (i in self) { + value = self[i]; + if (fun.call(thisp, value, i, object)) { + result.push(value); + } + } + } + return result; + }; +} +if (!Array.prototype.every) { + Array.prototype.every = function every(fun /*, thisp */) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0, + thisp = arguments[1]; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + + for (var i = 0; i < length; i++) { + if (i in self && !fun.call(thisp, self[i], i, object)) { + return false; + } + } + return true; + }; +} +if (!Array.prototype.some) { + Array.prototype.some = function some(fun /*, thisp */) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0, + thisp = arguments[1]; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + + for (var i = 0; i < length; i++) { + if (i in self && fun.call(thisp, self[i], i, object)) { + return true; + } + } + return false; + }; +} +if (!Array.prototype.reduce) { + Array.prototype.reduce = function reduce(fun /*, initial*/) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + if (!length && arguments.length == 1) { + throw new TypeError("reduce of empty array with no initial value"); + } + + var i = 0; + var result; + if (arguments.length >= 2) { + result = arguments[1]; + } else { + do { + if (i in self) { + result = self[i++]; + break; + } + if (++i >= length) { + throw new TypeError("reduce of empty array with no initial value"); + } + } while (true); + } + + for (; i < length; i++) { + if (i in self) { + result = fun.call(void 0, result, self[i], i, object); + } + } + + return result; + }; +} +if (!Array.prototype.reduceRight) { + Array.prototype.reduceRight = function reduceRight(fun /*, initial*/) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + if (!length && arguments.length == 1) { + throw new TypeError("reduceRight of empty array with no initial value"); + } + + var result, i = length - 1; + if (arguments.length >= 2) { + result = arguments[1]; + } else { + do { + if (i in self) { + result = self[i--]; + break; + } + if (--i < 0) { + throw new TypeError("reduceRight of empty array with no initial value"); + } + } while (true); + } + + do { + if (i in this) { + result = fun.call(void 0, result, self[i], i, object); + } + } while (i--); + + return result; + }; +} +if (!Array.prototype.indexOf || ([0, 1].indexOf(1, 2) != -1)) { + Array.prototype.indexOf = function indexOf(sought /*, fromIndex */ ) { + var self = splitString && _toString(this) == "[object String]" ? + this.split("") : + toObject(this), + length = self.length >>> 0; + + if (!length) { + return -1; + } + + var i = 0; + if (arguments.length > 1) { + i = toInteger(arguments[1]); + } + i = i >= 0 ? i : Math.max(0, length + i); + for (; i < length; i++) { + if (i in self && self[i] === sought) { + return i; + } + } + return -1; + }; +} +if (!Array.prototype.lastIndexOf || ([0, 1].lastIndexOf(0, -3) != -1)) { + Array.prototype.lastIndexOf = function lastIndexOf(sought /*, fromIndex */) { + var self = splitString && _toString(this) == "[object String]" ? + this.split("") : + toObject(this), + length = self.length >>> 0; + + if (!length) { + return -1; + } + var i = length - 1; + if (arguments.length > 1) { + i = Math.min(i, toInteger(arguments[1])); + } + i = i >= 0 ? i : length - Math.abs(i); + for (; i >= 0; i--) { + if (i in self && sought === self[i]) { + return i; + } + } + return -1; + }; +} +if (!Object.getPrototypeOf) { + Object.getPrototypeOf = function getPrototypeOf(object) { + return object.__proto__ || ( + object.constructor ? + object.constructor.prototype : + prototypeOfObject + ); + }; +} +if (!Object.getOwnPropertyDescriptor) { + var ERR_NON_OBJECT = "Object.getOwnPropertyDescriptor called on a " + + "non-object: "; + Object.getOwnPropertyDescriptor = function getOwnPropertyDescriptor(object, property) { + if ((typeof object != "object" && typeof object != "function") || object === null) + throw new TypeError(ERR_NON_OBJECT + object); + if (!owns(object, property)) + return; + + var descriptor, getter, setter; + descriptor = { enumerable: true, configurable: true }; + if (supportsAccessors) { + var prototype = object.__proto__; + object.__proto__ = prototypeOfObject; + + var getter = lookupGetter(object, property); + var setter = lookupSetter(object, property); + object.__proto__ = prototype; + + if (getter || setter) { + if (getter) descriptor.get = getter; + if (setter) descriptor.set = setter; + return descriptor; + } + } + descriptor.value = object[property]; + return descriptor; + }; +} +if (!Object.getOwnPropertyNames) { + Object.getOwnPropertyNames = function getOwnPropertyNames(object) { + return Object.keys(object); + }; +} +if (!Object.create) { + var createEmpty; + if (Object.prototype.__proto__ === null) { + createEmpty = function () { + return { "__proto__": null }; + }; + } else { + createEmpty = function () { + var empty = {}; + for (var i in empty) + empty[i] = null; + empty.constructor = + empty.hasOwnProperty = + empty.propertyIsEnumerable = + empty.isPrototypeOf = + empty.toLocaleString = + empty.toString = + empty.valueOf = + empty.__proto__ = null; + return empty; + } + } + + Object.create = function create(prototype, properties) { + var object; + if (prototype === null) { + object = createEmpty(); + } else { + if (typeof prototype != "object") + throw new TypeError("typeof prototype["+(typeof prototype)+"] != 'object'"); + var Type = function () {}; + Type.prototype = prototype; + object = new Type(); + object.__proto__ = prototype; + } + if (properties !== void 0) + Object.defineProperties(object, properties); + return object; + }; +} + +function doesDefinePropertyWork(object) { + try { + Object.defineProperty(object, "sentinel", {}); + return "sentinel" in object; + } catch (exception) { + } +} +if (Object.defineProperty) { + var definePropertyWorksOnObject = doesDefinePropertyWork({}); + var definePropertyWorksOnDom = typeof document == "undefined" || + doesDefinePropertyWork(document.createElement("div")); + if (!definePropertyWorksOnObject || !definePropertyWorksOnDom) { + var definePropertyFallback = Object.defineProperty; + } +} + +if (!Object.defineProperty || definePropertyFallback) { + var ERR_NON_OBJECT_DESCRIPTOR = "Property description must be an object: "; + var ERR_NON_OBJECT_TARGET = "Object.defineProperty called on non-object: " + var ERR_ACCESSORS_NOT_SUPPORTED = "getters & setters can not be defined " + + "on this javascript engine"; + + Object.defineProperty = function defineProperty(object, property, descriptor) { + if ((typeof object != "object" && typeof object != "function") || object === null) + throw new TypeError(ERR_NON_OBJECT_TARGET + object); + if ((typeof descriptor != "object" && typeof descriptor != "function") || descriptor === null) + throw new TypeError(ERR_NON_OBJECT_DESCRIPTOR + descriptor); + if (definePropertyFallback) { + try { + return definePropertyFallback.call(Object, object, property, descriptor); + } catch (exception) { + } + } + if (owns(descriptor, "value")) { + + if (supportsAccessors && (lookupGetter(object, property) || + lookupSetter(object, property))) + { + var prototype = object.__proto__; + object.__proto__ = prototypeOfObject; + delete object[property]; + object[property] = descriptor.value; + object.__proto__ = prototype; + } else { + object[property] = descriptor.value; + } + } else { + if (!supportsAccessors) + throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED); + if (owns(descriptor, "get")) + defineGetter(object, property, descriptor.get); + if (owns(descriptor, "set")) + defineSetter(object, property, descriptor.set); + } + + return object; + }; +} +if (!Object.defineProperties) { + Object.defineProperties = function defineProperties(object, properties) { + for (var property in properties) { + if (owns(properties, property)) + Object.defineProperty(object, property, properties[property]); + } + return object; + }; +} +if (!Object.seal) { + Object.seal = function seal(object) { + return object; + }; +} +if (!Object.freeze) { + Object.freeze = function freeze(object) { + return object; + }; +} +try { + Object.freeze(function () {}); +} catch (exception) { + Object.freeze = (function freeze(freezeObject) { + return function freeze(object) { + if (typeof object == "function") { + return object; + } else { + return freezeObject(object); + } + }; + })(Object.freeze); +} +if (!Object.preventExtensions) { + Object.preventExtensions = function preventExtensions(object) { + return object; + }; +} +if (!Object.isSealed) { + Object.isSealed = function isSealed(object) { + return false; + }; +} +if (!Object.isFrozen) { + Object.isFrozen = function isFrozen(object) { + return false; + }; +} +if (!Object.isExtensible) { + Object.isExtensible = function isExtensible(object) { + if (Object(object) === object) { + throw new TypeError(); // TODO message + } + var name = ''; + while (owns(object, name)) { + name += '?'; + } + object[name] = true; + var returnValue = owns(object, name); + delete object[name]; + return returnValue; + }; +} +if (!Object.keys) { + var hasDontEnumBug = true, + dontEnums = [ + "toString", + "toLocaleString", + "valueOf", + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable", + "constructor" + ], + dontEnumsLength = dontEnums.length; + + for (var key in {"toString": null}) { + hasDontEnumBug = false; + } + + Object.keys = function keys(object) { + + if ( + (typeof object != "object" && typeof object != "function") || + object === null + ) { + throw new TypeError("Object.keys called on a non-object"); + } + + var keys = []; + for (var name in object) { + if (owns(object, name)) { + keys.push(name); + } + } + + if (hasDontEnumBug) { + for (var i = 0, ii = dontEnumsLength; i < ii; i++) { + var dontEnum = dontEnums[i]; + if (owns(object, dontEnum)) { + keys.push(dontEnum); + } + } + } + return keys; + }; + +} +if (!Date.now) { + Date.now = function now() { + return new Date().getTime(); + }; +} +var ws = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003" + + "\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028" + + "\u2029\uFEFF"; +if (!String.prototype.trim || ws.trim()) { + ws = "[" + ws + "]"; + var trimBeginRegexp = new RegExp("^" + ws + ws + "*"), + trimEndRegexp = new RegExp(ws + ws + "*$"); + String.prototype.trim = function trim() { + return String(this).replace(trimBeginRegexp, "").replace(trimEndRegexp, ""); + }; +} + +function toInteger(n) { + n = +n; + if (n !== n) { // isNaN + n = 0; + } else if (n !== 0 && n !== (1/0) && n !== -(1/0)) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + return n; +} + +function isPrimitive(input) { + var type = typeof input; + return ( + input === null || + type === "undefined" || + type === "boolean" || + type === "number" || + type === "string" + ); +} + +function toPrimitive(input) { + var val, valueOf, toString; + if (isPrimitive(input)) { + return input; + } + valueOf = input.valueOf; + if (typeof valueOf === "function") { + val = valueOf.call(input); + if (isPrimitive(val)) { + return val; + } + } + toString = input.toString; + if (typeof toString === "function") { + val = toString.call(input); + if (isPrimitive(val)) { + return val; + } + } + throw new TypeError(); +} +var toObject = function (o) { + if (o == null) { // this matches both null and undefined + throw new TypeError("can't convert "+o+" to object"); + } + return Object(o); +}; + +}); From 26f5305593861b7bf416312400d43ca6b1d6e184 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 15 Nov 2016 23:22:25 +0000 Subject: [PATCH 3/8] Add jsonata function help --- Gruntfile.js | 6 +- editor/js/i18n.js | 2 +- editor/js/ui/common/typedInput.js | 5 +- editor/js/ui/editor.js | 23 ++++- editor/templates/index.mst | 8 +- editor/vendor/jsonata/formatter.js | 109 +++++++++++++++++++++ editor/vendor/jsonata/mode-jsonata.js | 14 ++- editor/vendor/jsonata/worker-jsonata.js | 121 ++++++++++++------------ red/api/index.js | 2 + red/api/locales/en-US/editor.json | 3 + red/api/locales/en-US/jsonata.json | 98 +++++++++++++++++++ 11 files changed, 318 insertions(+), 73 deletions(-) create mode 100644 editor/vendor/jsonata/formatter.js create mode 100644 red/api/locales/en-US/jsonata.json diff --git a/Gruntfile.js b/Gruntfile.js index 0e15c8dcd..dac1329a6 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -152,6 +152,10 @@ module.exports = function(grunt) { "public/vendor/vendor.css": [ // TODO: resolve relative resource paths in // bootstrap/FA/jquery + ], + "public/vendor/jsonata/jsonata.js": [ + "editor/vendor/jsonata/jsonata.js", + "editor/vendor/jsonata/formatter.js" ] } } @@ -161,7 +165,7 @@ module.exports = function(grunt) { files: { 'public/red/red.min.js': 'public/red/red.js', 'public/red/main.min.js': 'public/red/main.js', - 'public/vendor/jsonata/jsonata.min.js': 'editor/vendor/jsonata/jsonata.js', + 'public/vendor/jsonata/jsonata.min.js': 'public/vendor/jsonata/jsonata.js', 'public/vendor/ace/mode-jsonata.js': 'editor/vendor/jsonata/mode-jsonata.js', 'public/vendor/ace/worker-jsonata.js': 'editor/vendor/jsonata/worker-jsonata.js' } diff --git a/editor/js/i18n.js b/editor/js/i18n.js index cf29a6b43..f03b89a7b 100644 --- a/editor/js/i18n.js +++ b/editor/js/i18n.js @@ -23,7 +23,7 @@ RED.i18n = (function() { dynamicLoad: false, load:'current', ns: { - namespaces: ["editor","node-red"], + namespaces: ["editor","node-red","jsonata"], defaultNs: "editor" }, fallbackLng: ['en-US'], diff --git a/editor/js/ui/common/typedInput.js b/editor/js/ui/common/typedInput.js index c433c0366..6f18a4884 100644 --- a/editor/js/ui/common/typedInput.js +++ b/editor/js/ui/common/typedInput.js @@ -85,6 +85,7 @@ } return true; } + var allOptions = { msg: {value:"msg",label:"msg.",validate:validateExpression}, flow: {value:"flow",label:"flow.",validate:validateExpression}, @@ -103,9 +104,9 @@ expand:function() { var that = this; RED.editor.editExpression({ - value: this.value(), + value: jsonata.format(this.value()), complete: function(v) { - that.value(v); + that.value(v.replace(/\s*\n\s*/g," ")); } }) } diff --git a/editor/js/ui/editor.js b/editor/js/ui/editor.js index cb821d731..23947b9fb 100644 --- a/editor/js/ui/editor.js +++ b/editor/js/ui/editor.js @@ -1392,6 +1392,7 @@ RED.editor = (function() { text: RED._("common.label.done"), class: "primary", click: function() { + $("#node-input-expression-help").html(""); onComplete(expressionEditor.getValue()); RED.tray.close(); } @@ -1412,8 +1413,19 @@ RED.editor = (function() { }, open: function(tray) { var trayBody = tray.find('.editor-tray-body'); - var dialogForm = buildEditForm(tray,'dialog-form','_expression'); + var dialogForm = buildEditForm(tray,'dialog-form','_expression','editor'); + var funcSelect = $("#node-input-expression-func"); + jsonata.functions.forEach(function(f) { + funcSelect.append($("").val(f).text(f)); + }) + funcSelect.change(function(e) { + var f = $(this).val(); + var args = RED._('jsonata:'+f+".args",{defaultValue:''}); + var title = "

"+f+"("+args+")

"; + var body = marked(RED._('jsonata:'+f+'.desc',{defaultValue:''})); + $("#node-input-expression-help").html(title+"

"+body+"

"); + }) expressionEditor = RED.editor.createEditor({ id: 'node-input-expression', value: "", @@ -1426,6 +1438,15 @@ RED.editor = (function() { }); 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(); + } + }); + dialogForm.i18n(); }, diff --git a/editor/templates/index.mst b/editor/templates/index.mst index 8407774a3..7dc5fda50 100644 --- a/editor/templates/index.mst +++ b/editor/templates/index.mst @@ -161,7 +161,13 @@ diff --git a/editor/vendor/jsonata/formatter.js b/editor/vendor/jsonata/formatter.js new file mode 100644 index 000000000..036de6852 --- /dev/null +++ b/editor/vendor/jsonata/formatter.js @@ -0,0 +1,109 @@ +(function() { + function indentLine(str,length) { + if (length <= 0) { + return str; + } + var i = (new Array(length)).join(" "); + str = str.replace(/^\s*/,i); + return str; + } + function formatExpression(str) { + var length = str.length; + var start = 0; + var inString = false; + var inBox = false; + var quoteChar; + var list = []; + var stack = []; + var frame; + var v; + var matchingBrackets = { + "(":")", + "[":"]", + "{":"}" + } + for (var i=0;i 30) { + longStack.push(true); + indent += 4; + pre = result.substring(0,offset+f.pos+1); + post = result.substring(offset+f.pos+1); + indented = indentLine(post,indent); + result = pre+"\n"+indented; + offset += indented.length-post.length+1; + } else { + longStack.push(false); + } + } else if (f.type === "close-block") { + if (f.width > 30) { + indent -= 4; + pre = result.substring(0,offset+f.pos); + post = result.substring(offset+f.pos); + indented = indentLine(post,indent); + result = pre+"\n"+indented; + offset += indented.length-post.length+1; + } + longStack.pop(); + } + }) + //console.log(result); + return result; + } + + 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"] +})(); diff --git a/editor/vendor/jsonata/mode-jsonata.js b/editor/vendor/jsonata/mode-jsonata.js index 24562c536..50190e7b3 100644 --- a/editor/vendor/jsonata/mode-jsonata.js +++ b/editor/vendor/jsonata/mode-jsonata.js @@ -1,8 +1,10 @@ define("ace/mode/jsonata",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules","ace/worker/worker_client","ace/mode/text"], function(require, exports, module) { + "use strict"; var oop = require("../lib/oop"); var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; + var WorkerClient = require("../worker/worker_client").WorkerClient; var JSONataHighlightRules = function() { @@ -12,6 +14,8 @@ define("ace/mode/jsonata",["require","exports","module","ace/lib/oop","ace/mode/ "and|or|in", "constant.language": "null|Infinity|NaN|undefined", + "constant.language.boolean": + "true|false", "storage.type": "function", "keyword": @@ -39,16 +43,12 @@ define("ace/mode/jsonata",["require","exports","module","ace/lib/oop","ace/mode/ token : "constant.numeric", // float regex : /[+-]?\d[\d_]*(?:(?:\.\d*)?(?:[eE][+-]?\d+)?)?\b/ }, - { - token : keywordMapper, - regex : "[a-zA-Z\\$_\u00a1-\uffff][a-zA-Z\\d\\$_\u00a1-\uffff]*" - }, { token: "keyword", regex: /λ/ }, { - token: "constant.language.boolean", - regex: "true|false" + token : keywordMapper, + regex : "[a-zA-Z\\$_\u00a1-\uffff][a-zA-Z\\d\\$_\u00a1-\uffff]*" }, { token : "punctuation.operator", @@ -119,8 +119,6 @@ define("ace/mode/jsonata",["require","exports","module","ace/lib/oop","ace/mode/ return worker; }; - - this.$id = "ace/mode/jsonata"; }).call(Mode.prototype); diff --git a/editor/vendor/jsonata/worker-jsonata.js b/editor/vendor/jsonata/worker-jsonata.js index b6803d3af..da292ea6d 100644 --- a/editor/vendor/jsonata/worker-jsonata.js +++ b/editor/vendor/jsonata/worker-jsonata.js @@ -11,7 +11,7 @@ if (!window.console) { postMessage({type: "log", data: msgs}); }; window.console.error = - window.console.warn = + window.console.warn = window.console.log = window.console.trace = window.console; } @@ -23,7 +23,7 @@ window.onerror = function(message, file, line, col, err) { message: message, data: err.data, file: file, - line: line, + line: line, col: col, stack: err.stack }}); @@ -39,13 +39,13 @@ window.normalizeModule = function(parentId, moduleName) { if (moduleName.charAt(0) == ".") { var base = parentId.split("/").slice(0, -1).join("/"); moduleName = (base ? base + "/" : "") + moduleName; - + while (moduleName.indexOf(".") !== -1 && previous != moduleName) { var previous = moduleName; moduleName = moduleName.replace(/^\.\//, "").replace(/\/\.\//, "/").replace(/[^\/]+\/\.\.\//, ""); } } - + return moduleName; }; @@ -67,13 +67,13 @@ window.require = function require(parentId, id) { } return module.exports; } - + if (!window.require.tlns) return console.log("unable to load " + id); - + var path = resolveModuleId(id, window.require.tlns); if (path.slice(-3) != ".js") path += ".js"; - + window.require.id = id; window.require.modules[id] = {}; // prevent infinite loop on broken modules importScripts(path); @@ -112,7 +112,7 @@ window.define = function(id, deps, factory) { deps = []; id = window.require.id; } - + if (typeof factory != "function") { window.require.modules[id] = { exports: factory, @@ -163,13 +163,13 @@ window.initSender = function initSender() { var EventEmitter = window.require("ace/lib/event_emitter").EventEmitter; var oop = window.require("ace/lib/oop"); - + var Sender = function() {}; - + (function() { - + oop.implement(this, EventEmitter); - + this.callback = function(data, callbackId) { postMessage({ type: "call", @@ -177,7 +177,7 @@ window.initSender = function initSender() { data: data }); }; - + this.emit = function(name, data) { postMessage({ type: "event", @@ -185,9 +185,9 @@ window.initSender = function initSender() { data: data }); }; - + }).call(Sender.prototype); - + return new Sender(); }; @@ -517,7 +517,7 @@ function validateDelta(docLines, delta) { } exports.applyDelta = function(docLines, delta, doNotValidate) { - + var row = delta.start.row; var startColumn = delta.start.column; var line = docLines[row] || ""; @@ -582,7 +582,7 @@ EventEmitter._dispatchEvent = function(eventName, e) { if (e.propagationStopped) break; } - + if (defaultHandler && !e.defaultPrevented) return defaultHandler(e, this); }; @@ -610,7 +610,7 @@ EventEmitter.setDefaultHandler = function(eventName, callback) { var handlers = this._defaultHandlers if (!handlers) handlers = this._defaultHandlers = {_disabled_: {}}; - + if (handlers[eventName]) { var old = handlers[eventName]; var disabled = handlers._disabled_[eventName]; @@ -618,7 +618,7 @@ EventEmitter.setDefaultHandler = function(eventName, callback) { handlers._disabled_[eventName] = disabled = []; disabled.push(old); var i = disabled.indexOf(callback); - if (i != -1) + if (i != -1) disabled.splice(i, 1); } handlers[eventName] = callback; @@ -628,7 +628,7 @@ EventEmitter.removeDefaultHandler = function(eventName, callback) { if (!handlers) return; var disabled = handlers._disabled_[eventName]; - + if (handlers[eventName] == callback) { var old = handlers[eventName]; if (disabled) @@ -684,7 +684,7 @@ var EventEmitter = require("./lib/event_emitter").EventEmitter; var Anchor = exports.Anchor = function(doc, row, column) { this.$onChange = this.onChange.bind(this); this.attach(doc); - + if (typeof column == "undefined") this.setPosition(row.row, row.column); else @@ -707,16 +707,16 @@ var Anchor = exports.Anchor = function(doc, row, column) { if (delta.start.row > this.row) return; - + var point = $getTransformedPoint(delta, {row: this.row, column: this.column}, this.$insertRight); this.setPosition(point.row, point.column, true); }; - + function $pointsInOrder(point1, point2, equalPointsInOrder) { var bColIsAfter = equalPointsInOrder ? point1.column <= point2.column : point1.column < point2.column; return (point1.row < point2.row) || (point1.row == point2.row && bColIsAfter); } - + function $getTransformedPoint(delta, point, moveIfEqual) { var deltaIsInsert = delta.action == "insert"; var deltaRowShift = (deltaIsInsert ? 1 : -1) * (delta.end.row - delta.start.row); @@ -735,7 +735,7 @@ var Anchor = exports.Anchor = function(doc, row, column) { column: point.column + (point.row == deltaEnd.row ? deltaColShift : 0) }; } - + return { row: deltaStart.row, column: deltaStart.column @@ -919,23 +919,23 @@ var Document = function(textOrLines) { this.insert = function(position, text) { if (this.getLength() <= 1) this.$detectNewLine(text); - + return this.insertMergedLines(position, this.$split(text)); }; this.insertInLine = function(position, text) { var start = this.clippedPos(position.row, position.column); var end = this.pos(position.row, position.column + text.length); - + this.applyDelta({ start: start, end: end, action: "insert", lines: [text] }, true); - + return this.clonePos(end); }; - + this.clippedPos = function(row, column) { var length = this.getLength(); if (row === undefined) { @@ -952,15 +952,15 @@ var Document = function(textOrLines) { column = Math.min(Math.max(column, 0), line.length); return {row: row, column: column}; }; - + this.clonePos = function(pos) { return {row: pos.row, column: pos.column}; }; - + this.pos = function(row, column) { return {row: row, column: column}; }; - + this.$clipPosition = function(position) { var length = this.getLength(); if (position.row >= length) { @@ -984,21 +984,21 @@ var Document = function(textOrLines) { column = this.$lines[row].length; } this.insertMergedLines({row: row, column: column}, lines); - }; + }; this.insertMergedLines = function(position, lines) { var start = this.clippedPos(position.row, position.column); var end = { row: start.row + lines.length - 1, column: (lines.length == 1 ? start.column : 0) + lines[lines.length - 1].length }; - + this.applyDelta({ start: start, end: end, action: "insert", lines: lines }); - + return this.clonePos(end); }; this.remove = function(range) { @@ -1015,14 +1015,14 @@ var Document = function(textOrLines) { this.removeInLine = function(row, startColumn, endColumn) { var start = this.clippedPos(row, startColumn); var end = this.clippedPos(row, endColumn); - + this.applyDelta({ start: start, end: end, action: "remove", lines: this.getLinesForRange({start: start, end: end}) }, true); - + return this.clonePos(start); }; this.removeFullLines = function(firstRow, lastRow) { @@ -1033,10 +1033,10 @@ var Document = function(textOrLines) { var startRow = ( deleteFirstNewLine ? firstRow - 1 : firstRow ); var startCol = ( deleteFirstNewLine ? this.getLine(startRow).length : 0 ); var endRow = ( deleteLastNewLine ? lastRow + 1 : lastRow ); - var endCol = ( deleteLastNewLine ? 0 : this.getLine(endRow).length ); + var endCol = ( deleteLastNewLine ? 0 : this.getLine(endRow).length ); var range = new Range(startRow, startCol, endRow, endCol); var deletedLines = this.$lines.slice(firstRow, lastRow + 1); - + this.applyDelta({ start: range.start, end: range.end, @@ -1071,7 +1071,7 @@ var Document = function(textOrLines) { else { end = range.start; } - + return end; }; this.applyDeltas = function(deltas) { @@ -1090,17 +1090,17 @@ var Document = function(textOrLines) { : !Range.comparePoints(delta.start, delta.end)) { return; } - + if (isInsert && delta.lines.length > 20000) this.$splitAndapplyLargeDelta(delta, 20000); applyDelta(this.$lines, delta, doNotValidate); this._signal("change", delta); }; - + this.$splitAndapplyLargeDelta = function(delta, MAX) { var lines = delta.lines; var l = lines.length; - var row = delta.start.row; + var row = delta.start.row; var column = delta.start.column; var from = 0, to = 0; do { @@ -1203,7 +1203,7 @@ exports.copyArray = function(array){ for (var i=0, l=array.length; i Date: Wed, 16 Nov 2016 13:44:45 +0000 Subject: [PATCH 4/8] Preserve newlines in jsonata expression via tabs --- editor/js/ui/common/typedInput.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/editor/js/ui/common/typedInput.js b/editor/js/ui/common/typedInput.js index 6f18a4884..e7a26cf3d 100644 --- a/editor/js/ui/common/typedInput.js +++ b/editor/js/ui/common/typedInput.js @@ -104,9 +104,9 @@ expand:function() { var that = this; RED.editor.editExpression({ - value: jsonata.format(this.value()), + value: this.value().replace(/\t/g,"\n"), complete: function(v) { - that.value(v.replace(/\s*\n\s*/g," ")); + that.value(v.replace(/\n/g,"\t")); } }) } From eeaff6b553dcd0a5e593885a182dea79e8f3b8c0 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 16 Nov 2016 14:54:51 +0000 Subject: [PATCH 5/8] Add insert-function button to expression editor --- Gruntfile.js | 3 ++- editor/js/ui/editor.js | 17 +++++++++++++---- editor/templates/index.mst | 2 +- editor/vendor/jsonata/jsonata.js | 3 +++ red/api/locales/en-US/editor.json | 3 ++- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index dac1329a6..9c44b782e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -167,7 +167,8 @@ module.exports = function(grunt) { 'public/red/main.min.js': 'public/red/main.js', 'public/vendor/jsonata/jsonata.min.js': 'public/vendor/jsonata/jsonata.js', 'public/vendor/ace/mode-jsonata.js': 'editor/vendor/jsonata/mode-jsonata.js', - 'public/vendor/ace/worker-jsonata.js': 'editor/vendor/jsonata/worker-jsonata.js' + 'public/vendor/ace/worker-jsonata.js': 'editor/vendor/jsonata/worker-jsonata.js', + 'public/vendor/ace/snippets/jsonata.js': 'editor/vendor/jsonata/snippets-jsonata.js' } } }, diff --git a/editor/js/ui/editor.js b/editor/js/ui/editor.js index 23947b9fb..5e9775ec8 100644 --- a/editor/js/ui/editor.js +++ b/editor/js/ui/editor.js @@ -1421,7 +1421,7 @@ RED.editor = (function() { funcSelect.change(function(e) { var f = $(this).val(); var args = RED._('jsonata:'+f+".args",{defaultValue:''}); - var title = "

"+f+"("+args+")

"; + var title = "
"+f+"("+args+")
"; var body = marked(RED._('jsonata:'+f+'.desc',{defaultValue:''})); $("#node-input-expression-help").html(title+"

"+body+"

"); @@ -1432,7 +1432,7 @@ RED.editor = (function() { mode:"ace/mode/jsonata", options: { enableBasicAutocompletion:true, - enableSnippets:false, + enableSnippets:true, enableLiveAutocompletion: true } }); @@ -1441,14 +1441,23 @@ RED.editor = (function() { expressionEditor.on("changeSelection", function() { var c = expressionEditor.getCursorPosition(); var token = expressionEditor.getSession().getTokenAt(c.row,c.column); - //console.log(token); + // console.log(token); if (token && token.type === 'keyword') { funcSelect.val(token.value).change(); } }); dialogForm.i18n(); - + $("#node-input-expression-func-insert").click(function(e) { + 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); + expressionEditor.focus(); + }) }, close: function() { editStack.pop(); diff --git a/editor/templates/index.mst b/editor/templates/index.mst index 7dc5fda50..d8cbb2537 100644 --- a/editor/templates/index.mst +++ b/editor/templates/index.mst @@ -166,7 +166,7 @@
- +
diff --git a/editor/vendor/jsonata/jsonata.js b/editor/vendor/jsonata/jsonata.js index 2883b5fc4..814bba28f 100644 --- a/editor/vendor/jsonata/jsonata.js +++ b/editor/vendor/jsonata/jsonata.js @@ -2788,6 +2788,9 @@ var jsonata = (function() { }, assign: function (name, value) { environment.bind(name, value); + }, + ast: function() { + return ast; } }; } diff --git a/red/api/locales/en-US/editor.json b/red/api/locales/en-US/editor.json index 5029b6182..dca0d7ad7 100644 --- a/red/api/locales/en-US/editor.json +++ b/red/api/locales/en-US/editor.json @@ -320,6 +320,7 @@ "empty": "No matches found" }, "expressionEditor": { - "functions": "Functions" + "functions": "Functions", + "insert": "Insert" } } From eaa4b76ede164ff84512a4dcd85c7053d3cea706 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 18 Nov 2016 16:38:48 +0000 Subject: [PATCH 6/8] Update jsonata version --- editor/vendor/jsonata/jsonata.js | 299 +++++++++++++++---------------- package.json | 2 +- 2 files changed, 150 insertions(+), 151 deletions(-) diff --git a/editor/vendor/jsonata/jsonata.js b/editor/vendor/jsonata/jsonata.js index 814bba28f..6c4ff877e 100644 --- a/editor/vendor/jsonata/jsonata.js +++ b/editor/vendor/jsonata/jsonata.js @@ -4,7 +4,6 @@ * This project is licensed under the MIT License, see LICENSE */ -'use strict'; /** * @module JSONata * @description JSON query and transformation language @@ -17,6 +16,8 @@ * @returns {{evaluate: evaluate, assign: assign}} Evaluated expression */ var jsonata = (function() { + 'use strict'; + var operators = { '.': 75, '[': 80, @@ -141,7 +142,7 @@ var jsonata = (function() { position += 4; } else { throw { - message: "The escape sequence \\u must be followed by 4 hex digits at column " + position, + message: "The escape sequence \\u must be followed by 4 hex digits", stack: (new Error()).stack, position: position }; @@ -149,7 +150,7 @@ var jsonata = (function() { } else { // illegal escape sequence throw { - message: 'unsupported escape sequence: \\' + currentChar + ' at column ' + position, + message: 'unsupported escape sequence: \\' + currentChar, stack: (new Error()).stack, position: position, token: currentChar @@ -165,7 +166,7 @@ var jsonata = (function() { position++; } throw { - message: 'no terminating quote found in string literal at column ' + position, + message: 'no terminating quote found in string literal', stack: (new Error()).stack, position: position }; @@ -180,7 +181,7 @@ var jsonata = (function() { return create('number', num); } else { throw { - message: 'Number out of range: ' + match[0] + ' at column ' + position, + message: 'Number out of range: ' + match[0], stack: (new Error()).stack, position: position, token: match[0] @@ -270,7 +271,7 @@ var jsonata = (function() { // unexpected end of buffer msg = "Syntax error: expected '" + id + "' before end of expression"; } else { - msg = "Syntax error: expected '" + id + "', got '" + node.id + "' at column " + node.position; + msg = "Syntax error: expected '" + id + "', got '" + node.id; } throw { message: msg , @@ -297,7 +298,7 @@ var jsonata = (function() { symbol = symbol_table[value]; if (!symbol) { throw { - message: "Unknown operator: " + value + " at column " + next_token.position, + message: "Unknown operator: " + value, stack: (new Error()).stack, position: next_token.position, token: value @@ -310,10 +311,10 @@ var jsonata = (function() { type = "literal"; symbol = symbol_table["(literal)"]; break; - /* istanbul ignore next */ + /* istanbul ignore next */ default: throw { - message: "Unexpected token:" + value + " at column " + next_token.position, + message: "Unexpected token:" + value, stack: (new Error()).stack, position: next_token.position, token: value @@ -483,28 +484,7 @@ var jsonata = (function() { return this; }); - // object constructor - prefix("{", function () { - 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("}"); - this.lhs = a; - this.type = "unary"; - return this; - }); - - // array constructor + // array constructor prefix("[", function () { var a = []; if (node.id !== "]") { @@ -532,21 +512,57 @@ var jsonata = (function() { // filter - predicate or array index infix("[", operators['['], function (left) { - this.lhs = left; - this.rhs = expression(operators[']']); - this.type = 'binary'; - advance("]"); - return this; + 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; + } }); - // aggregator - infix("{", operators['{'], function (left) { - this.lhs = left; - this.rhs = expression(operators['}']); - this.type = 'binary'; + 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) { @@ -600,13 +616,16 @@ var jsonata = (function() { case 'binary': switch (expr.value) { case '.': - var step = post_parse(expr.lhs); - if (Array.isArray(step)) { - Array.prototype.push.apply(result, step); + var lstep = post_parse(expr.lhs); + if (lstep.type === 'path') { + Array.prototype.push.apply(result, lstep); } else { - result.push(step); + result.push(lstep); + } + var rest = post_parse(expr.rhs); + if(rest.type !== 'path') { + rest = [rest]; } - var rest = [post_parse(expr.rhs)]; Array.prototype.push.apply(result, rest); result.type = 'path'; break; @@ -615,31 +634,41 @@ var jsonata = (function() { // LHS is a step or a predicated step // RHS is the predicate expr result = post_parse(expr.lhs); - if (typeof result.aggregate !== 'undefined') { + var step = result; + if(result.type === 'path') { + step = result[result.length - 1]; + } + if (typeof step.group !== 'undefined') { throw { - message: 'A predicate cannot follow an aggregate in a step. Error at column: ' + expr.position, + 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 result.predicate === 'undefined') { - result.predicate = []; + if (typeof step.predicate === 'undefined') { + step.predicate = []; } - result.predicate.push(post_parse(expr.rhs)); + step.predicate.push(post_parse(expr.rhs)); break; case '{': - // aggregate + // group-by // LHS is a step or a predicated step - // RHS is the predicate expr + // RHS is the object constructor expr result = post_parse(expr.lhs); - if (typeof result.aggregate !== 'undefined') { + if (typeof result.group !== 'undefined') { throw { - message: 'Each step can only have one aggregator. Error at column: ' + expr.position, + message: 'Each step can only have one grouping expression. Error at column: ' + expr.position, stack: (new Error()).stack, position: expr.position }; } - result.aggregate = post_parse(expr.rhs); + // 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}; @@ -700,6 +729,9 @@ var jsonata = (function() { // 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': @@ -716,7 +748,7 @@ var jsonata = (function() { result = expr; } else { throw { - message: "Syntax error: " + expr.value + " at column " + expr.position, + message: "Syntax error: " + expr.value, stack: (new Error()).stack, position: expr.position, token: expr.value @@ -724,7 +756,7 @@ var jsonata = (function() { } break; default: - var reason = "Unknown expression type: " + expr.value + " at column " + expr.position; + var reason = "Unknown expression type: " + expr.value; /* istanbul ignore else */ if (expr.id === '(end)') { reason = "Syntax error: unexpected end of expression"; @@ -747,7 +779,7 @@ var jsonata = (function() { var expr = expression(0); if (node.id !== '(end)') { throw { - message: "Syntax error: " + node.value + " at column " + node.position, + message: "Syntax error: " + node.value, stack: (new Error()).stack, position: node.position, token: node.value @@ -755,12 +787,6 @@ var jsonata = (function() { } expr = post_parse(expr); - // a single name token is a single step location path - if (expr.type === 'name') { - expr = [expr]; - expr.type = 'path'; - } - return expr; }; @@ -865,8 +891,8 @@ var jsonata = (function() { if (expr.hasOwnProperty('predicate')) { result = applyPredicates(expr.predicate, result, environment); } - if (expr.hasOwnProperty('aggregate')) { - result = applyAggregate(expr.aggregate, result, environment); + if (expr.hasOwnProperty('group')) { + result = evaluateGroupExpression(expr.group, result, environment); } var exitCallback = environment.lookup('__evaluate_exit'); @@ -887,6 +913,7 @@ var jsonata = (function() { 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 @@ -898,7 +925,11 @@ var jsonata = (function() { } // evaluate each step in turn - expr.forEach(function (step) { + 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 @@ -913,35 +944,36 @@ var jsonata = (function() { if (expr.length > 1 && step.type === 'literal') { step.type = 'name'; } - if (step.value === '{') { - if(typeof input !== 'undefined') { - result = evaluateGroupExpression(step, inputSequence, environment); - } - } else { - inputSequence.forEach(function (item) { - var res = evaluate(step, item, environment); - if (typeof res !== 'undefined') { - if (Array.isArray(res)) { - // 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); - } + 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) { - result = resultSequence[0]; - } else if (resultSequence.length > 1) { - result = resultSequence; } + }); + 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; } @@ -1016,35 +1048,6 @@ var jsonata = (function() { return result; } - /** - * Apply aggregate to input data - * @param {Object} expr - JSONata expression - * @param {Object} input - Input data to evaluate against - * @param {Object} environment - Environment - * @returns {{}} Result after applying aggregate - */ - function applyAggregate(expr, input, environment) { - var result = {}; - // this is effectively a 'reduce' HOF (fold left) - // if the input is a singleton, then just return this as the result - // otherwise iterate over the input array and aggregate the result - if (Array.isArray(input)) { - // create a new frame to limit the scope - var aggEnv = createFrame(environment); - // the variable $@ will hold the aggregated value, initialize this to the first array item - aggEnv.bind('_', input[0]); - // loop over the remainder of the array - for (var index = 1; index < input.length; index++) { - var reduce = evaluate(expr, input[index], aggEnv); - aggEnv.bind('_', reduce); - } - result = aggEnv.lookup('_'); - } else { - result = input; - } - return result; - } - /** * Evaluate binary expression against input data * @param {Object} expr - JSONata expression @@ -1108,7 +1111,7 @@ var jsonata = (function() { result = -result; } else { throw { - message: "Cannot negate a non-numeric value: " + result + " at column " + expr.position, + message: "Cannot negate a non-numeric value: " + result, stack: (new Error()).stack, position: expr.position, token: expr.value, @@ -1122,11 +1125,10 @@ var jsonata = (function() { expr.lhs.forEach(function (item) { var value = evaluate(item, input, environment); if (typeof value !== 'undefined') { - if (item.value === '..') { - // array generated by the range operator - merge into results - result = functionAppend(result, value); - } else { + if(item.value === '[') { result.push(value); + } else { + result = functionAppend(result, value); } } }); @@ -1289,7 +1291,7 @@ var jsonata = (function() { if (!isNumeric(lhs)) { throw { - message: 'LHS of ' + expr.value + ' operator must evaluate to a number at column ' + expr.position, + message: 'LHS of ' + expr.value + ' operator must evaluate to a number', stack: (new Error()).stack, position: expr.position, token: expr.value, @@ -1298,7 +1300,7 @@ var jsonata = (function() { } if (!isNumeric(rhs)) { throw { - message: 'RHS of ' + expr.value + ' operator must evaluate to a number at column ' + expr.position, + message: 'RHS of ' + expr.value + ' operator must evaluate to a number', stack: (new Error()).stack, position: expr.position, token: expr.value, @@ -1413,11 +1415,11 @@ var jsonata = (function() { switch (expr.value) { case 'and': result = functionBoolean(evaluate(expr.lhs, input, environment)) && - functionBoolean(evaluate(expr.rhs, input, environment)); + functionBoolean(evaluate(expr.rhs, input, environment)); break; case 'or': result = functionBoolean(evaluate(expr.lhs, input, environment)) || - functionBoolean(evaluate(expr.rhs, input, environment)); + functionBoolean(evaluate(expr.rhs, input, environment)); break; } return result; @@ -1468,7 +1470,7 @@ var jsonata = (function() { // key has to be a string if (typeof key !== 'string') { throw { - message: 'Key in object structure must evaluate to a string. Got: ' + key + ' at column ' + expr.position, + message: 'Key in object structure must evaluate to a string. Got: ' + key, stack: (new Error()).stack, position: expr.position, value: key @@ -1518,7 +1520,7 @@ var jsonata = (function() { if (!Number.isInteger(lhs)) { throw { - message: 'LHS of range operator (..) must evaluate to an integer at column ' + expr.position, + message: 'LHS of range operator (..) must evaluate to an integer', stack: (new Error()).stack, position: expr.position, token: expr.value, @@ -1527,7 +1529,7 @@ var jsonata = (function() { } if (!Number.isInteger(rhs)) { throw { - message: 'RHS of range operator (..) must evaluate to an integer at column ' + expr.position, + message: 'RHS of range operator (..) must evaluate to an integer', stack: (new Error()).stack, position: expr.position, token: expr.value, @@ -1555,11 +1557,11 @@ var jsonata = (function() { 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 $) at column " + expr.position, + 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.value + value: expr.lhs.type === 'path' ? expr.lhs[0].value : expr.lhs.value }; } environment.bind(expr.lhs.value, value); @@ -1645,13 +1647,13 @@ var jsonata = (function() { // 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 === 'name' && environment.lookup(expr.procedure.value)) { + 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 at column ' + expr.position + '. Did you mean \'$' + expr.procedure.value + '\'?', + message: 'Attempted to invoke a non-function. Did you mean \'$' + expr.procedure[0].value + '\'?', stack: (new Error()).stack, position: expr.position, - token: expr.procedure.value + token: expr.procedure[0].value }; } // apply the procedure @@ -1673,7 +1675,7 @@ var jsonata = (function() { // add the position field to the error err.position = expr.position; // and the function identifier - err.token = expr.procedure.value; + err.token = expr.procedure.type === 'path' ? expr.procedure[0].value : expr.procedure.value; throw err; } return result; @@ -1745,13 +1747,13 @@ var jsonata = (function() { }); // lookup the procedure var proc = evaluate(expr.procedure, input, environment); - if (typeof proc === 'undefined' && expr.procedure.type === 'name' && environment.lookup(expr.procedure.value)) { + 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 at column ' + expr.position + '. Did you mean \'$' + expr.procedure.value + '\'?', + 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.value + token: expr.procedure[0].value }; } if (proc && proc.lambda) { @@ -1760,10 +1762,10 @@ var jsonata = (function() { result = partialApplyNativeFunction(proc, evaluatedArgs); } else { throw { - message: 'Attempted to partially apply a non-function at column ' + expr.position, + message: 'Attempted to partially apply a non-function', stack: (new Error()).stack, position: expr.position, - token: expr.procedure.value + token: expr.procedure.type === 'path' ? expr.procedure[0].value : expr.procedure.value }; } return result; @@ -2097,8 +2099,8 @@ var jsonata = (function() { } 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; + (val && isLambda(val)) ? '' : + (typeof val === 'function') ? '' : val; }); return str; } @@ -2788,9 +2790,6 @@ var jsonata = (function() { }, assign: function (name, value) { environment.bind(name, value); - }, - ast: function() { - return ast; } }; } diff --git a/package.json b/package.json index 33de70e92..819697fec 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "fs.notify":"0.0.4", "i18next":"1.10.6", "is-utf8":"0.2.1", - "jsonata":"1.0.7", + "jsonata":"1.0.10", "media-typer": "0.3.0", "mqtt": "1.14.1", "mustache": "2.2.1", From 534b07d120cb34f40153b497df6d8d9c3eecdd57 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 23 Nov 2016 23:15:30 +0000 Subject: [PATCH 7/8] 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]*" From 659c326f8977bc0edb6270dd762f488dd031389d Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 23 Nov 2016 23:16:17 +0000 Subject: [PATCH 8/8] Add jsonata snippets --- editor/vendor/jsonata/snippets-jsonata.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 editor/vendor/jsonata/snippets-jsonata.js diff --git a/editor/vendor/jsonata/snippets-jsonata.js b/editor/vendor/jsonata/snippets-jsonata.js new file mode 100644 index 000000000..3fa8b7e82 --- /dev/null +++ b/editor/vendor/jsonata/snippets-jsonata.js @@ -0,0 +1,11 @@ +define("ace/snippets/jsonata",["require","exports","module"], function(require, exports, module) { +"use strict"; +var snippetText = ""; +for (var fn in jsonata.functions) { + if (jsonata.functions.hasOwnProperty(fn)) { + snippetText += "# "+fn+"\nsnippet "+fn+"\n\t"+jsonata.getFunctionSnippet(fn)+"\n" + } +} +exports.snippetText = snippetText; +exports.scope = "jsonata"; +});